Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1726f0160 | ||
|
|
bd1841b788 | ||
|
|
bde35d1d31 | ||
|
|
8d6a1be777 | ||
|
|
56f34ba362 | ||
|
|
4d329e046f | ||
|
|
f3977153fb | ||
|
|
274bedd186 | ||
|
|
2e4dbe7f7f | ||
|
|
0334e443eb | ||
|
|
76f5ed5c96 | ||
|
|
18f588dc24 | ||
|
|
491c686762 | ||
|
|
25303df677 | ||
|
|
ae0d63b86f | ||
|
|
41ade2e205 | ||
|
|
0a9d332d60 | ||
|
|
1983f7705f | ||
|
|
6b2bf0ba70 | ||
|
|
6d9715169c | ||
|
|
0645a3712a | ||
|
|
ebc32ea965 | ||
|
|
078db33458 | ||
|
|
04f5cbe31f | ||
|
|
b5a7d8d559 | ||
|
|
58f8485b02 | ||
|
|
3e1da9c335 | ||
|
|
6bf6206e1c | ||
|
|
f9c60951c9 | ||
|
|
06b3f28df0 | ||
|
|
89f124250c | ||
|
|
66f13fd6a7 | ||
|
|
a81d9cb940 | ||
|
|
13b8871200 | ||
|
|
2792c05c1c | ||
|
|
6ccfc88acb | ||
|
|
7f1d59b33a | ||
|
|
e4e8b108d2 | ||
|
|
242661a9c9 | ||
|
|
ca3e2f316c | ||
|
|
6ff4aa5f34 | ||
|
|
1eb54b8e6e | ||
|
|
4a6c424540 |
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
||||
# dockerignore
|
||||
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
.github/**
|
||||
Dockerfile*
|
||||
resources/
|
||||
node_modules/
|
||||
*.log
|
||||
tmp/
|
||||
.env
|
||||
.vscode/
|
||||
.DS_Store
|
||||
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1,2 +1,4 @@
|
||||
public/api.html linguist-documentation
|
||||
public/openapi.json linguist-documentation
|
||||
public/openapi.json linguist-documentation
|
||||
resources/ export-ignore
|
||||
.github/ export-ignore
|
||||
3
.github/workflows/sync-changelog.yml
vendored
3
.github/workflows/sync-changelog.yml
vendored
@@ -5,6 +5,9 @@ on:
|
||||
paths:
|
||||
- 'CHANGELOG.md'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
342
CHANGELOG.md
342
CHANGELOG.md
@@ -1,5 +1,343 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 5/4/2025 v1.3.1
|
||||
|
||||
### Modals
|
||||
|
||||
- **Added** a shared `.editor-close-btn` component for all modals:
|
||||
- File Tags
|
||||
- User Panel
|
||||
- TOTP Login & Setup
|
||||
- Change Password
|
||||
- **Truncated** long filenames in the File Tags modal header using CSS `text-overflow: ellipsis`.
|
||||
- **Resized** File Tags modal from 400px to 450px wide (with `max-width: 90vw` fallback).
|
||||
- **Capped** User Panel height at 381px and hidden scrollbars to eliminate layout jumps on hover.
|
||||
|
||||
### HTML
|
||||
|
||||
- **Moved** `<div id="loginForm">…</div>` out of `.main-wrapper` so the login form can show independently of the app shell.
|
||||
- **Added** `<div id="loadingOverlay"></div>` immediately inside `<body>` to cover the UI during auth checks.
|
||||
- **Inserted** inline `<style>` in `<head>` to:
|
||||
- Hide `.main-wrapper` by default.
|
||||
- Style `#loadingOverlay` as a full-viewport white overlay.
|
||||
|
||||
- **Added** `addUserModal`, `removeUserModal` & `renameFileModal` modals to `style="display:none;"`
|
||||
|
||||
### `main.js`
|
||||
|
||||
- **Extracted** `initializeApp()` helper to centralize post-auth startup (tag search, file list, drag-and-drop, folder tree, upload, trash/restore, admin config).
|
||||
- **Updated** DOMContentLoaded `checkAuthentication()` flow to call `initializeApp()` when already authenticated.
|
||||
- **Extended** `updateAuthenticatedUI()` to call `initializeApp()` after a fresh login so all UI modules re-hydrate.
|
||||
- **Enhanced** setup-mode in `checkAuthentication()`:
|
||||
- Show `#addUserModal` as a flex overlay (`style.display = 'flex'`).
|
||||
- Keep `.main-wrapper` hidden until setup completes.
|
||||
- **Added** post-setup handler in the Add-User modal’s save button:
|
||||
- Hide setup modal.
|
||||
- Show login form.
|
||||
- Keep app shell hidden.
|
||||
- Pre-fill and focus the new username in the login inputs.
|
||||
|
||||
### `auth.js` / Auth Logic
|
||||
|
||||
- **Refactored** `checkAuthentication()` to handle three states:
|
||||
1. **`data.setup`** remove overlay, hide main UI, show setup modal.
|
||||
2. **`data.authenticated`** remove overlay, call `updateAuthenticatedUI()`.
|
||||
3. **not authenticated** remove overlay, show login form, keep main UI hidden.
|
||||
- **Refined** `updateAuthenticatedUI()` to:
|
||||
- Remove loading overlay.
|
||||
- Show `.main-wrapper` and main operations.
|
||||
- Hide `#loginForm`.
|
||||
- Reveal header buttons.
|
||||
- Initialize dynamic header buttons (restore, admin, user-panel).
|
||||
- Call `initializeApp()` to load all modules after login.
|
||||
|
||||
---
|
||||
|
||||
## Changes 5/3/2025 v1.3.0
|
||||
|
||||
**Admin Panel Refactor & Enhancements**
|
||||
|
||||
### Moved from `authModals.js` to `adminPanel.js`
|
||||
|
||||
- Extracted all admin-related UI and logic out of `authModals.js`
|
||||
- Created a standalone `adminPanel.js` module
|
||||
- Initialized `openAdminPanel()` and `closeAdminPanel()` exports
|
||||
|
||||
### Responsive, Collapsible Sections
|
||||
|
||||
- Injected new CSS via JS (`adminPanelStyles`)
|
||||
- Default modal width: 50%
|
||||
- Small-screen override (`@media (max-width: 600px)`) to 90% width
|
||||
- Introduced `.section-header` / `.section-content` pattern
|
||||
- Click header to expand/collapse its content
|
||||
- Animated arrow via Material Icons
|
||||
- Indented and padded expanded content
|
||||
|
||||
### “Manage Shared Links” Feature
|
||||
|
||||
- Added new **Manage Shared Links** section to Admin Panel
|
||||
- Endpoint **GET** `/api/admin/readMetadata.php?file=…`
|
||||
- Reads `share_folder_links.json` & `share_links.json` under `META_DIR`
|
||||
- Endpoint **POST**
|
||||
- `/api/folder/deleteShareFolderLink.php`
|
||||
- `/api/file/deleteShareLink.php`
|
||||
- `loadShareLinksSection()` AJAX loader
|
||||
- Displays folder & file shares, expiry dates, upload-allowed, and 🔒 if password-protected
|
||||
- “🗑️” delete buttons refresh the list on success
|
||||
|
||||
### Dark-Mode & Theming Fixes
|
||||
|
||||
- Dark-mode CSS overrides for:
|
||||
- Modal border
|
||||
- `.btn-primary`, `.btn-secondary`
|
||||
- `.form-control` backgrounds & placeholders
|
||||
- Section headers & icons
|
||||
- Close button restyled to use shared **.editor-close-btn** look
|
||||
|
||||
### API and Controller changes
|
||||
|
||||
- Updated all endpoints to use correct controller casing
|
||||
- Renamed controller files to PascalCase (e.g. `adminController.php` to `AdminController.php`, `fileController.php` to `FileController.php`, `folderController.php` to `FolderController.php`)
|
||||
- Adjusted endpoint paths to match controller filenames
|
||||
- Fix FolderController readOnly create folder permission
|
||||
|
||||
### Additional changes
|
||||
|
||||
- Extend clean up expired shared entries
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/30/2025 v1.2.8
|
||||
|
||||
- **Added** PDF preview in `filePreview.js` (the `extension === "pdf"` block): replaced in-modal `<embed>` with `window.open(urlWithTs, "_blank")` and closed the modal to avoid CSP `frame-ancestors 'none'` restrictions.
|
||||
- **Added** `autofocus` attribute to the login form’s username input (`#loginUsername`) so the cursor is ready for typing on page load.
|
||||
- **Enhanced** login initialization with a `DOMContentLoaded` fallback that calls `loginUsername.focus()` (via `setTimeout`) if needed.
|
||||
- **Set** focus to the “New Username” field (`#newUsername`) when entering setup mode, hiding the login form and showing the Add-User modal.
|
||||
- **Implemented** Enter-key support in setup mode by attaching `attachEnterKeyListener("addUserModal", "saveUserBtn")`, allowing users to press Enter to submit the Add-User form.
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/28/2025
|
||||
|
||||
**Added**
|
||||
|
||||
- **Custom expiration** option to File Share modal
|
||||
- Users can specify a value + unit (seconds, minutes, hours, days)
|
||||
- Displays a warning when a custom duration is selected
|
||||
- **Custom expiration** option to Folder Share modal (same value+unit picker and warning)
|
||||
|
||||
**Changed**
|
||||
|
||||
- **API parameters** for both endpoints:
|
||||
- Replaced `expirationMinutes` with `expirationValue` + `expirationUnit`
|
||||
- Front-end now sends `{ expirationValue, expirationUnit }`
|
||||
- Back-end converts those into total seconds before saving
|
||||
- **UI**
|
||||
- FileShare and FolderShare modals updated to handle “Custom…” selection
|
||||
|
||||
**Updated Models & Controllers**
|
||||
|
||||
- **FileModel::createShareLink** now accepts expiration in seconds
|
||||
- **FolderModel::createShareFolderLink** now accepts expiration in seconds
|
||||
- **createShareLink.php** & **createShareFolderLink.php** updated to parse and convert new parameters
|
||||
|
||||
**Documentation**
|
||||
|
||||
- OpenAPI annotations for both endpoints updated to require `expirationValue` + `expirationUnit` (enum: seconds, minutes, hours, days)
|
||||
|
||||
## Changes 4/27/2025 v1.2.7
|
||||
|
||||
- **Select-All** checkbox now correctly toggles all `.file-checkbox` inputs
|
||||
- Updated `toggleAllCheckboxes(masterCheckbox)` to call `updateRowHighlight()` on each row so selections get the `.row-selected` highlight
|
||||
- **Master checkbox sync** in toolbar
|
||||
- Enhanced `updateFileActionButtons()` to set the header checkbox to checked, unchecked, or indeterminate based on how many files are selected
|
||||
- Fixed Pagination controls & Items-per-page dropdown
|
||||
- Fixed `#advancedSearchToggle` in both `renderFileTable()` and `renderGalleryView()`
|
||||
- **Shared folder gallery view logic**
|
||||
- Introduced new `public/js/sharedFolderView.js` containing all DOMContentLoaded wiring, `toggleViewMode()`, gallery rendering, and event listeners
|
||||
- Embedded a non-executing JSON payload in `shareFolder.php`
|
||||
- **`FolderController::shareFolder()` / `shareFolder.php`**
|
||||
- Removed all inline `onclick="…"` attributes and inline `<script>` blocks
|
||||
- Added `<script type="application/json" id="shared-data">…</script>` to export `$token` and `$files`
|
||||
- Added `<script src="/js/sharedFolderView.js" defer></script>` to load the external view logic
|
||||
- **Styling updates**
|
||||
- Added `.toggle-btn` CSS for blue header-style toggle button and applied it in JS
|
||||
- Added `.pagination a:hover { background-color: #0056b3; }` to match button hover
|
||||
- Tweaked `body` padding and `header h1` margins to reduce whitespace above header
|
||||
- Refactored `sharedFolderView.js:renderGalleryView()` to eliminate `innerHTML` usage; now uses `document.createElement` and `textContent` so filenames and URLs are fully escaped and CSP-safe
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/26/2025 1.2.6
|
||||
|
||||
**Apache / Dockerfile (CSP)**
|
||||
|
||||
- Enabled Apache’s `mod_headers` in the Dockerfile (`a2enmod headers ssl deflate expires proxy proxy_fcgi rewrite`)
|
||||
- Added a strong `Content-Security-Policy` header in the vhost configs to lock down allowed sources for scripts, styles, fonts, images, and connections
|
||||
|
||||
**index.html & CDN Includes**
|
||||
|
||||
- Applied Subresource Integrity (`integrity` + `crossorigin="anonymous"`) to all static CDN assets (Bootstrap CSS, CodeMirror CSS/JS, Resumable.js, DOMPurify, Fuse.js)
|
||||
- Omitted SRI on Google Fonts & Material Icons links (dynamic per-browser CSS)
|
||||
- Removed all inline `<script>` and `onclick` attributes; now all behaviors live in external JS modules
|
||||
|
||||
**auth.js (Logout Handling)**
|
||||
|
||||
- Moved the logout-on-`?logout=1` snippet from inline HTML into `auth.js`
|
||||
- In `DOMContentLoaded`, attached a `click` listener to `#logoutBtn` that POSTs to `/api/auth/logout.php` and reloads
|
||||
|
||||
**fileActions.js (Modal Button Handlers)**
|
||||
|
||||
- Externalized the cancel/download buttons for single-file and ZIP-download modals by adding `click` listeners in `fileActions.js`
|
||||
- Removed the inline `onclick` attributes from `#cancelDownloadFile` and `#confirmSingleDownloadButton` in the HTML
|
||||
- Ensured all file-action modals (delete, download, extract, copy, move, rename) now use JS event handlers instead of inline code
|
||||
|
||||
**domUtils.js**
|
||||
|
||||
- **Removed** all inline `onclick` and `onchange` attributes from:
|
||||
- `buildSearchAndPaginationControls` (advanced search toggle, prev/next buttons, items-per-page selector)
|
||||
- `buildFileTableHeader` (select-all checkbox)
|
||||
- `buildFileTableRow` (download, edit, preview, rename buttons)
|
||||
- **Retained** all original logic (file-type icon detection, shift-select, debounce, custom confirm modal, etc.)
|
||||
|
||||
**fileListView.js**
|
||||
|
||||
- **Stopped** generating inline `onclick` handlers in both table and gallery views.
|
||||
- **Added** `data-` attributes on actionable elements:
|
||||
- `data-download-name`, `data-download-folder`
|
||||
- `data-edit-name`, `data-edit-folder`
|
||||
- `data-rename-name`, `data-rename-folder`
|
||||
- `data-preview-url`, `data-preview-name`
|
||||
- IDs on controls: `#advancedSearchToggle`, `#searchInput`, `#prevPageBtn`, `#nextPageBtn`, `#selectAll`, `#itemsPerPageSelect`
|
||||
- **Introduced** `attachListControlListeners()` to bind all events via `addEventListener` immediately after rendering, preserving every interaction without inline code.
|
||||
|
||||
**Additional changes**
|
||||
|
||||
- **Security**: Added `frame-src 'self'` to the Content-Security-Policy header so that the embedded API docs iframe can load from our own origin without relaxing JS restrictions.
|
||||
- **Controller**: Updated `FolderController::shareFolder()` (folderController) to include the gallery-view toggle script block intact, ensuring the “Switch to Gallery View” button works when sharing folders.
|
||||
- **UI (fileListView.js)**: Refactored `renderGalleryView` to remove all inline `onclick=` handlers; switched to using data-attributes and `addEventListener()` for preview, download, edit and rename buttons, fully CSP-compliant.
|
||||
- Moved logout button handler out of inline `<script>` in `index.html` and into the `DOMContentLoaded` init in **main.js** (via `auth.js`), so it now attaches reliably after the CSRF token is loaded and DOM is ready.
|
||||
- Added Content-Security-Policy for `<Files "api.php">` block to allow embedding the ReDoc iframe.
|
||||
- Extracted inline ReDoc init into `public/js/redoc-init.js` and updated `public/api.php` to use deferred `<script>` tags.
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/25/2025
|
||||
|
||||
- Switch single‐file download to native `<a>` link (no JS buffering)
|
||||
- Keep spinner modal during ZIP creation and download blob on POST response
|
||||
- Replace text toggle with a single button showing sun/moon icons and hover tooltip
|
||||
|
||||
## Changes 4/24/2025 1.2.5
|
||||
|
||||
- Enhance README and wiki with expanded installation instructions
|
||||
- Adjusted Dockerfile’s Apache vhost to:
|
||||
- Alias `/uploads/` to `/var/www/uploads/` with PHP engine disabled and directory indexes off
|
||||
- Disable HTTP TRACE and tune keep-alive (On, max 100 requests, 5s timeout) and server Timeout (60s)
|
||||
- Add security headers (`X-Frame-Options`, `X-Content-Type-Options`, `X-XSS-Protection`, `Referrer-Policy`)
|
||||
- Enable `mod_deflate` compression for HTML, plain text, CSS, JS and JSON
|
||||
- Configure `mod_expires` caching for images (1 month), CSS (1 week) and JS (3 hour)
|
||||
- Deny access to hidden files (dot-files)
|
||||
~~- Add access control in public/.htaccess for api.html & openapi.json; update Nginx example in wiki~~
|
||||
- Remove obsolete folders from repo root
|
||||
- Embed API documentation (`api.php`) directly in the FileRise UI as a full-screen modal
|
||||
- Introduced `openApiModalBtn` in the user panel to launch the API modal
|
||||
- Added `#apiModal` container with a same-origin `<iframe src="api.php">` so session cookies authenticate automatically
|
||||
- Close control uses the existing `.editor-close-btn` for consistent styling and hover effects
|
||||
|
||||
- public/api.html has been replaced by the new api.php wrapper
|
||||
- **`public/api.php`**
|
||||
- Single PHP endpoint for both UI and spec
|
||||
- Enforces `$_SESSION['authenticated']`
|
||||
- Renders the Redoc API docs when accessed normally
|
||||
- Streams the JSON spec from `openapi.json.dist` when called as `api.php?spec=1`
|
||||
- Redirects unauthenticated users to `index.html?redirect=/api.php`
|
||||
- **Moved** `public/openapi.json` → `openapi.json.dist` (moved outside of `public/`) to prevent direct static access
|
||||
- **Dockerfile**: enabled required Apache modules for rewrite, security headers, proxying, caching and compression:
|
||||
|
||||
```dockerfile
|
||||
RUN a2enmod rewrite headers proxy proxy_fcgi expires deflate
|
||||
```
|
||||
|
||||
## Changes 4/23/2025 1.2.4
|
||||
|
||||
**AuthModel**
|
||||
|
||||
- **Added** `validateRememberToken(string $token): ?array`
|
||||
- Reads and decrypts `persistent_tokens.json`
|
||||
- Verifies token exists and hasn’t expired
|
||||
- Returns stored payload (`username`, `expiry`, `isAdmin`, etc.) or `null` if invalid
|
||||
|
||||
**authController (checkAuth)**
|
||||
|
||||
- **Enhanced** “remember-me” re-login path at top of `checkAuth()`
|
||||
- Calls `AuthModel::validateRememberToken()` when session is missing but `remember_me_token` cookie present
|
||||
- Repopulates `$_SESSION['authenticated']`, `username`, `isAdmin`, `folderOnly`, `readOnly`, `disableUpload` from payload
|
||||
- Regenerates session ID and CSRF token, then immediately returns JSON and exits
|
||||
|
||||
- **Updated** `userController.php`
|
||||
- Fixed totp isAdmin when session is missing but `remember_me_token` cookie present
|
||||
|
||||
- **loadCsrfToken()**
|
||||
- Now reads `X-CSRF-Token` response header first, falls back to JSON `csrf_token` if header absent
|
||||
- Updates `window.csrfToken`, `window.SHARE_URL`, and `<meta>` tags with the new values
|
||||
- **fetchWithCsrf(url, options)**
|
||||
- Sends `credentials: 'include'` and current `X-CSRF-Token` on every request
|
||||
- Handles “soft-failure” JSON (`{ csrf_expired: true, csrf_token }`): updates token and retries once without a 403 in DevTools
|
||||
- On HTTP 403 fallback: reads new token from header or `/api/auth/token.php`, updates token, and retries once
|
||||
|
||||
- **start.sh**
|
||||
- Session directory setup
|
||||
|
||||
- Always sends `credentials: 'include'` and `X-CSRF-Token: window.csrfToken` s
|
||||
- On HTTP 403, automatically fetches a fresh CSRF token (from the response header or `/api/auth/token.php`) and retries the request once
|
||||
- Always returns the real `Response` object (no more “clone.json” on every 200)
|
||||
- Now calls `fetchWithCsrf('/api/auth/token.php')` to guarantee a fresh token
|
||||
- Checks `res.ok`, then parses JSON to extract `csrf_token` and `share_url`
|
||||
- Updates both `window.csrfToken` and the `<meta name="csrf-token">` & `<meta name="share-url">` tags
|
||||
- Removed Old CSRF logic that cloned every successful response and parsed its JSON body
|
||||
- Removed Any “soft-failure” JSON peek on non-403 responses
|
||||
- Add missing permissions in `UserModel.php` for TOTP login.
|
||||
- **Prevent XSS in breadcrumbs**
|
||||
- Replaced `innerHTML` calls in `fileListTitle` with a new `updateBreadcrumbTitle()` helper that uses `textContent` + `DocumentFragment`.
|
||||
- Introduced `renderBreadcrumbFragment()` to build each breadcrumb segment as a `<span class="breadcrumb-link" data-folder="…">` node.
|
||||
- Added `setupBreadcrumbDelegation()` to handle clicks via event delegation on the container, eliminating per-element listeners.
|
||||
- Removed any raw HTML concatenation to satisfy CodeQL and ensure all breadcrumb text is safely escaped.
|
||||
|
||||
## Changes 4/22/2025 v1.2.3
|
||||
|
||||
- Support for custom PUID/PGID via `PUID`/`PGID` environment variables, replacing the need to run the container with `--user`
|
||||
- New `PUID` and `PGID` config options in the Unraid Community Apps template
|
||||
- Dockerfile:
|
||||
- startup (`start.sh`) now runs as root to write `/etc/php` & `/etc/apache2` configs
|
||||
- `www‑data` user is remapped at build‑time to the supplied `PUID:PGID`, then Apache drops privileges to that user
|
||||
- Unraid template: removed recommendation to use `--user`; replaced with `PUID`, `PGID`, and `Container Port` variables
|
||||
- “Permission denied” errors when forcing `--user 99:100` on Unraid by ensuring startup runs as root
|
||||
- Dockerfile silence group issue
|
||||
- `enableWebDAV` toggle in Admin Panel (default: disabled)
|
||||
- **Admin Panel enhancements**
|
||||
- New `enableWebDAV` boolean setting
|
||||
- New `sharedMaxUploadSize` numeric setting (bytes)
|
||||
- **Shared Folder upload size**
|
||||
- `sharedMaxUploadSize` is now enforced in `FolderModel::uploadToSharedFolder`
|
||||
- Upload form header on shared‑folder page dynamically shows “(X MB max size)”
|
||||
- **API updates**
|
||||
- `getConfig` and `updateConfig` endpoints now include `enableWebDAV` and `sharedMaxUploadSize`
|
||||
- Updated `AdminModel` & `AdminController` to persist and validate new settings
|
||||
- Enhanced `shareFolder()` view to pull from admin config and format the max‑upload‑size label
|
||||
- Restored the MIT license copyright line that was inadvertently removed.
|
||||
- Move .htaccess to public folder this was mistake since API refactor.
|
||||
- gitattributes to ignore resources/ & .github/ on export
|
||||
- Hardened `Dockerfile` permissions: all code files owned by `root:www-data` (dirs `755`, files `644`), only `uploads/`, `users/` and `metadata/` are writable by `www-data` (`775`)
|
||||
- `.dockerignore` entry to exclude the `.github` directory from build context
|
||||
- `start.sh`:
|
||||
- Creates and secures `metadata/log` for Apache logs
|
||||
- Dynamically creates and sets permissions on `uploads`, `users`, and `metadata` directories at startup
|
||||
- Apache VirtualHost updated to redirect `ErrorLog` and `CustomLog` into `/var/www/metadata/log`
|
||||
- docker: remove symlink add alias for uploads folder
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/21/2025 v1.2.2
|
||||
|
||||
### Added
|
||||
@@ -40,7 +378,7 @@
|
||||
Refactored to:
|
||||
1. Fetch CSRF
|
||||
2. POST credentials to `/api/auth/auth.php`
|
||||
3. On `totp_required`, re‑fetch CSRF *again* before calling `openTOTPLoginModal()`
|
||||
3. On `totp_required`, re‑fetch CSRF again before calling `openTOTPLoginModal()`
|
||||
4. Handle full logins vs. TOTP flows cleanly.
|
||||
|
||||
- **TOTP handlers update**
|
||||
@@ -986,7 +1324,7 @@ The enhancements extend the existing drag-and-drop functionality by adding a hea
|
||||
- Adjusted file preview and icon styling for better alignment.
|
||||
- Centered the header and optimized the layout for a clean, modern appearance.
|
||||
|
||||
*This changelog and feature summary reflect the improvements made during the refactor from a monolithic utils file to modular ES6 components, along with enhancements in UI responsiveness, sorting, file uploads, and file management operations.*
|
||||
This changelog and feature summary reflect the improvements made during the refactor from a monolithic utils file to modular ES6 components, along with enhancements in UI responsiveness, sorting, file uploads, and file management operations.
|
||||
|
||||
---
|
||||
|
||||
|
||||
136
Dockerfile
136
Dockerfile
@@ -6,12 +6,9 @@
|
||||
FROM ubuntu:24.04 AS appsource
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
rm -rf /var/lib/apt/lists/* # clean up apt cache
|
||||
|
||||
# prepare the folder and remove Apache’s default index
|
||||
RUN mkdir -p /var/www && rm -f /var/www/html/index.html
|
||||
|
||||
# **Copy the FileRise source** (where your composer.json lives)
|
||||
COPY . /var/www
|
||||
|
||||
#############################
|
||||
@@ -19,88 +16,123 @@ COPY . /var/www
|
||||
#############################
|
||||
FROM composer:2 AS composer
|
||||
WORKDIR /app
|
||||
|
||||
# **Copy composer files from the source** and install
|
||||
COPY --from=appsource /var/www/composer.json /var/www/composer.lock ./
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
RUN composer install --no-dev --optimize-autoloader # production-ready autoloader
|
||||
|
||||
#############################
|
||||
# Final Stage – runtime image
|
||||
#############################
|
||||
FROM ubuntu:24.04
|
||||
|
||||
LABEL by=error311
|
||||
|
||||
# Set basic environment variables
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
HOME=/root \
|
||||
LC_ALL=C.UTF-8 \
|
||||
LANG=en_US.UTF-8 \
|
||||
LANGUAGE=en_US.UTF-8 \
|
||||
TERM=xterm \
|
||||
UPLOAD_MAX_FILESIZE=5G \
|
||||
POST_MAX_SIZE=5G \
|
||||
TOTAL_UPLOAD_SIZE=5G \
|
||||
PERSISTENT_TOKENS_KEY=default_please_change_this_key
|
||||
|
||||
ARG PUID=99
|
||||
ARG PGID=100
|
||||
LC_ALL=C.UTF-8 LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 TERM=xterm \
|
||||
UPLOAD_MAX_FILESIZE=5G POST_MAX_SIZE=5G TOTAL_UPLOAD_SIZE=5G \
|
||||
PERSISTENT_TOKENS_KEY=default_please_change_this_key \
|
||||
PUID=99 PGID=100
|
||||
|
||||
# Install Apache, PHP, and required extensions
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
apache2 \
|
||||
php \
|
||||
php-json \
|
||||
php-curl \
|
||||
php-zip \
|
||||
php-mbstring \
|
||||
php-gd \
|
||||
php-xml \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
openssl && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
apache2 php php-json php-curl php-zip php-mbstring php-gd php-xml \
|
||||
ca-certificates curl git openssl && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* # slim down image
|
||||
|
||||
# Fix www-data UID/GID
|
||||
# Remap www-data to the PUID/PGID provided for safe bind mounts
|
||||
RUN set -eux; \
|
||||
if [ "$(id -u www-data)" != "${PUID}" ]; then usermod -u ${PUID} www-data || true; fi; \
|
||||
if [ "$(id -g www-data)" != "${PGID}" ]; then groupmod -g ${PGID} www-data || true; fi; \
|
||||
usermod -g ${PGID} www-data
|
||||
if [ "$(id -u www-data)" != "${PUID}" ]; then usermod -u "${PUID}" www-data; fi; \
|
||||
if [ "$(id -g www-data)" != "${PGID}" ]; then groupmod -g "${PGID}" www-data 2>/dev/null || true; fi; \
|
||||
usermod -g "${PGID}" www-data
|
||||
|
||||
# Copy application code and vendor directory
|
||||
# Copy config, code, and vendor
|
||||
COPY custom-php.ini /etc/php/8.3/apache2/conf.d/99-app-tuning.ini
|
||||
COPY --from=appsource /var/www /var/www
|
||||
COPY --from=composer /app/vendor /var/www/vendor
|
||||
COPY --from=composer /app/vendor /var/www/vendor
|
||||
|
||||
# Fix ownership & permissions
|
||||
RUN chown -R www-data:www-data /var/www && chmod -R 775 /var/www
|
||||
# Secure permissions: code read-only, only data dirs writable
|
||||
RUN chown -R root:www-data /var/www && \
|
||||
find /var/www -type d -exec chmod 755 {} \; && \
|
||||
find /var/www -type f -exec chmod 644 {} \; && \
|
||||
mkdir -p /var/www/public/uploads /var/www/users /var/www/metadata && \
|
||||
chown -R www-data:www-data /var/www/public/uploads /var/www/users /var/www/metadata && \
|
||||
chmod -R 775 /var/www/public/uploads /var/www/users /var/www/metadata # writable upload areas
|
||||
|
||||
# Create a symlink for uploads folder in public directory.
|
||||
RUN cd /var/www/public && ln -s ../uploads uploads
|
||||
|
||||
# Configure Apache
|
||||
# Apache site configuration
|
||||
RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
|
||||
<VirtualHost *:80>
|
||||
# Global settings
|
||||
TraceEnable off
|
||||
KeepAlive On
|
||||
MaxKeepAliveRequests 100
|
||||
KeepAliveTimeout 5
|
||||
Timeout 60
|
||||
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/public
|
||||
|
||||
# Security headers for all responses
|
||||
<IfModule mod_headers.c>
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self'; frame-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
|
||||
</IfModule>
|
||||
|
||||
# Compression
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json
|
||||
</IfModule>
|
||||
|
||||
# Cache static assets
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive on
|
||||
ExpiresByType image/jpeg "access plus 1 month"
|
||||
ExpiresByType image/png "access plus 1 month"
|
||||
ExpiresByType text/css "access plus 1 week"
|
||||
ExpiresByType application/javascript "access plus 3 hour"
|
||||
</IfModule>
|
||||
|
||||
# Protect uploads directory
|
||||
Alias /uploads/ /var/www/uploads/
|
||||
<Directory "/var/www/uploads/">
|
||||
Options -Indexes
|
||||
AllowOverride None
|
||||
<IfModule mod_php7.c>
|
||||
php_flag engine off
|
||||
</IfModule>
|
||||
<IfModule mod_php.c>
|
||||
php_flag engine off
|
||||
</IfModule>
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# Public directory
|
||||
<Directory "/var/www/public">
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
DirectoryIndex index.php index.html
|
||||
DirectoryIndex index.html index.php
|
||||
</Directory>
|
||||
ErrorLog /var/log/apache2/error.log
|
||||
CustomLog /var/log/apache2/access.log combined
|
||||
|
||||
# Deny access to hidden files
|
||||
<FilesMatch "^\.">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
<Files "api.php">
|
||||
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.redoc.ly; style-src 'self' 'unsafe-inline'; worker-src 'self' https://cdn.redoc.ly blob:; connect-src 'self'; img-src 'self' data: blob:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';"
|
||||
</Files>
|
||||
|
||||
ErrorLog /var/www/metadata/log/error.log
|
||||
CustomLog /var/www/metadata/log/access.log combined
|
||||
</VirtualHost>
|
||||
EOF
|
||||
|
||||
# Enable the rewrite and headers modules
|
||||
RUN a2enmod rewrite headers
|
||||
# Enable required modules
|
||||
RUN a2enmod rewrite headers proxy proxy_fcgi expires deflate ssl
|
||||
|
||||
# Expose ports and set up start script
|
||||
EXPOSE 80 443
|
||||
COPY start.sh /usr/local/bin/start.sh
|
||||
RUN chmod +x /usr/local/bin/start.sh
|
||||
|
||||
1
LICENSE
1
LICENSE
@@ -1,5 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 SeNS
|
||||
Copyright (c) 2025 FileRise
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
||||
22
README.md
22
README.md
@@ -1,7 +1,7 @@
|
||||
# FileRise
|
||||
|
||||
**Elevate your File Management** – A modern, self-hosted web file manager.
|
||||
Upload, organize, and share files through a sleek web interface. **FileRise** is lightweight yet powerful: think of it as your personal cloud drive that you control. With drag-and-drop uploads, in-browser editing, secure user logins (with SSO and 2FA support), and one-click sharing, **FileRise** makes file management on your server a breeze.
|
||||
Upload, organize, and share files or folders through a sleek web interface. **FileRise** is lightweight yet powerful: think of it as your personal cloud drive that you control. With drag-and-drop uploads, in-browser editing, secure user logins (with SSO and 2FA support), and one-click sharing, **FileRise** makes file management on your server a breeze.
|
||||
|
||||
**4/3/2025 Video demo:**
|
||||
|
||||
@@ -20,7 +20,7 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
|
||||
|
||||
- 🗃️ **Folder Sharing & File Sharing:** Easily share entire folders via secure, expiring public links. Folder shares can be password-protected, and shared folders support file uploads from outside users with a separate, secure upload mechanism. Folder listings are paginated (10 items per page) with navigation controls, and file sizes are displayed in MB for clarity. Share files with others using one-time or expiring public links (with password protection if desired) – convenient for sending individual files without exposing the whole app.
|
||||
|
||||
- 🔌 **WebDAV Support:** Mount FileRise as a network drive or connect via any WebDAV client. Supports standard file operations (upload/download/rename/delete) and direct `curl`/CLI access for scripting and automation. FolderOnly users are restricted to their personal folder, while admins and unrestricted users have full access. Compatible with Cyberduck, WinSCP, native OS drive mounts, and more.
|
||||
- 🔌 **WebDAV Support:** Mount FileRise as a network drive **or use it head‑less from the CLI**. Standard WebDAV operations (upload / download / rename / delete) work in Cyberduck, WinSCP, GNOME Files, Finder, etc., and you can also script against it with `curl` – see the [WebDAV](https://github.com/error311/FileRise/wiki/WebDAV) + [curl](https://github.com/error311/FileRise/wiki/Accessing-FileRise-via-curl%C2%A0(WebDAV)) quick‑start for examples. Folder‑Only users are restricted to their personal directory, while admins and unrestricted users have full access.
|
||||
|
||||
- 📚 **API Documentation:** Fully auto‑generated OpenAPI spec (`openapi.json`) and interactive HTML docs (`api.html`) powered by Redoc.
|
||||
|
||||
@@ -108,16 +108,16 @@ FileRise will be accessible at `http://localhost:8080` (or your server’s IP).
|
||||
|
||||
If you prefer to run FileRise on a traditional web server (LAMP stack or similar):
|
||||
|
||||
- **Requirements:** PHP 8.1 or higher, Apache (with mod_php) or another web server configured for PHP. Ensure PHP extensions json, curl, and zip are enabled. No database needed.
|
||||
- **Requirements:** PHP 8.3 or higher, Apache (with mod_php) or another web server configured for PHP. Ensure PHP extensions json, curl, and zip are enabled. No database needed.
|
||||
- **Download Files:** Clone this repo or download the [latest release archive](https://github.com/error311/FileRise/releases).
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/error311/FileRise.git
|
||||
```
|
||||
|
||||
Place the files into your web server’s directory (e.g., `/var/www/html/filerise`). It can be in a subfolder (just adjust the `BASE_URL` in config as below).
|
||||
Place the files into your web server’s directory (e.g., `/var/www/`). It can be in a subfolder (just adjust the `BASE_URL` in config as below).
|
||||
|
||||
- **Composer Dependencies:** If you plan to use OIDC (SSO login), install Composer and run `composer install` in the FileRise directory. (This pulls in a couple of PHP libraries like jumbojett/openid-connect for OAuth support.) If you skip this, FileRise will still work, but OIDC login won’t be available.
|
||||
- **Composer Dependencies:** Install Composer and run `composer install` in the FileRise directory. (This pulls in a couple of PHP libraries like jumbojett/openid-connect for OAuth support.)
|
||||
|
||||
- **Folder Permissions:** Ensure the server can write to the following directories (create them if they don’t exist):
|
||||
|
||||
@@ -149,7 +149,7 @@ Now navigate to the FileRise URL in your browser. On first load, you’ll be pro
|
||||
|
||||
## Quick‑start: Mount via WebDAV
|
||||
|
||||
Once FileRise is running, you can mount it like any other network drive:
|
||||
Once FileRise is running, you must enable WebDAV in admin panel to access it.
|
||||
|
||||
```bash
|
||||
# Linux (GVFS/GIO)
|
||||
@@ -232,19 +232,25 @@ Areas where you can help: translations, bug fixes, UI improvements, or building
|
||||
- **[phpseclib/phpseclib](https://github.com/phpseclib/phpseclib)** (v~3.0.7)
|
||||
- **[robthree/twofactorauth](https://github.com/RobThree/TwoFactorAuth)** (v^3.0)
|
||||
- **[endroid/qr-code](https://github.com/endroid/qr-code)** (v^5.0)
|
||||
- **[sabre/dav"](https://github.com/sabre-io/dav)** (^4.4)
|
||||
- **[sabre/dav](https://github.com/sabre-io/dav)** (^4.4)
|
||||
|
||||
### Client-Side Libraries
|
||||
|
||||
- **Google Fonts** – [Roboto](https://fonts.google.com/specimen/Roboto) and **Material Icons** ([Google Material Icons](https://fonts.google.com/icons))
|
||||
- **[Bootstrap](https://getbootstrap.com/)** (v4.5.2)
|
||||
- **[CodeMirror](https://codemirror.net/)** (v5.65.5) – For code editing functionality.
|
||||
- **[Resumable.js](http://www.resumablejs.com/)** (v1.1.0) – For file uploads.
|
||||
- **[Resumable.js](https://github.com/23/resumable.js/)** (v1.1.0) – For file uploads.
|
||||
- **[DOMPurify](https://github.com/cure53/DOMPurify)** (v2.4.0) – For sanitizing HTML.
|
||||
- **[Fuse.js](https://fusejs.io/)** (v6.6.2) – For indexed, fuzzy searching.
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Based on [uploader](https://github.com/sensboston/uploader) by @sensboston.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is open-source under the MIT License. That means you’re free to use, modify, and distribute **FileRise**, with attribution. We hope you find it useful and contribute back!
|
||||
|
||||
@@ -41,6 +41,7 @@ upload_tmp_dir=/tmp
|
||||
session.gc_maxlifetime=1440
|
||||
session.gc_probability=1
|
||||
session.gc_divisor=100
|
||||
session.save_path = "/var/www/sessions"
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Error Handling / Logging
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<!-- public/api.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>FileRise API Docs</title>
|
||||
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js" integrity="sha384-4vOjrBu7SuDWXcAw1qFznVLA/sKL+0l4nn+J1HY8w7cpa6twQEYuh4b0Cwuo7CyX" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="openapi.json"></redoc>
|
||||
<div id="redoc-container"></div>
|
||||
<script>
|
||||
// If the <redoc> tag didn’t render, fall back to init()
|
||||
if (!customElements.get('redoc')) {
|
||||
Redoc.init('openapi.json', {}, document.getElementById('redoc-container'));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
31
public/api.php
Normal file
31
public/api.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
// public/api.php
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
|
||||
if (empty($_SESSION['authenticated'])) {
|
||||
header('Location: /index.html?redirect=/api.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['spec'])) {
|
||||
header('Content-Type: application/json');
|
||||
readfile(__DIR__ . '/../openapi.json.dist');
|
||||
exit;
|
||||
}
|
||||
|
||||
?><!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<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-4vOjrBu7SuDWXcAw1qFznVLA/sKL+0l4nn+J1HY8w7cpa6twQEYuh4b0Cwuo7CyX"
|
||||
crossorigin="anonymous"></script>
|
||||
<script defer src="/js/redoc-init.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="api.php?spec=1"></redoc>
|
||||
<div id="redoc-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/addUser.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->addUser();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/admin/getConfig.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/adminController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||
|
||||
$adminController = new AdminController();
|
||||
$adminController->getConfig();
|
||||
63
public/api/admin/readMetadata.php
Normal file
63
public/api/admin/readMetadata.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
// public/api/admin/readMetadata.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
|
||||
// Only admins may read these
|
||||
if (empty($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Must supply ?file=share_links.json or share_folder_links.json
|
||||
if (empty($_GET['file'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing `file` parameter']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = basename($_GET['file']);
|
||||
$allowed = ['share_links.json', 'share_folder_links.json'];
|
||||
if (!in_array($file, $allowed, true)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Invalid file requested']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = META_DIR . $file;
|
||||
if (!file_exists($path)) {
|
||||
// Return empty object so JS sees `{}` not an error
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode((object)[]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$jsonData = file_get_contents($path);
|
||||
$data = json_decode($jsonData, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Corrupted JSON']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ——— Clean up expired entries ———
|
||||
$now = time();
|
||||
$changed = false;
|
||||
foreach ($data as $token => $entry) {
|
||||
if (!empty($entry['expires']) && $entry['expires'] < $now) {
|
||||
unset($data[$token]);
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
// overwrite file with cleaned data
|
||||
file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
// ——— Send cleaned data back ———
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/admin/updateConfig.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/adminController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||
|
||||
$adminController = new AdminController();
|
||||
$adminController->updateConfig();
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->auth();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/auth/checkAuth.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->checkAuth();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/auth/login_basic.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->loginBasic();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/auth/logout.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->logout();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/auth/token.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->getToken();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/changePassword.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->changePassword();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/copyFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->copyFiles();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/createShareLink.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->createShareLink();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/deleteFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->deleteFiles();
|
||||
6
public/api/file/deleteShareLink.php
Normal file
6
public/api/file/deleteShareLink.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->deleteShareLink();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/deleteTrashFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->deleteTrashFiles();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/download.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->downloadFile();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/downloadZip.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->downloadZip();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/extractZip.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->extractZip();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/getFileList.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getFileList();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/getFileTag.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getFileTags();
|
||||
6
public/api/file/getShareLinks.php
Normal file
6
public/api/file/getShareLinks.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getShareLinks();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/getTrashItems.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getTrashItems();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/moveFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->moveFiles();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/renameFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->renameFile();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/restoreFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->restoreFiles();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/saveFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->saveFile();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/saveFileTag.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->saveFileTag();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/share.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->shareFile();
|
||||
@@ -1,2 +0,0 @@
|
||||
cd /var/www/public
|
||||
ln -s ../uploads uploads
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/createFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->createFolder();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/createShareFolderLink.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->createShareFolderLink();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/deleteFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->deleteFolder();
|
||||
6
public/api/folder/deleteShareFolderLink.php
Normal file
6
public/api/folder/deleteShareFolderLink.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->deleteShareFolderLink();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/downloadSharedFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->downloadSharedFile();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/getFolderList.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->getFolderList();
|
||||
6
public/api/folder/getShareFolderLinks.php
Normal file
6
public/api/folder/getShareFolderLinks.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->getShareFolderLinks();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/renameFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->renameFolder();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/shareFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->shareFolder();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/uploadToSharedFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->uploadToSharedFolder();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/getUserPermissions.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->getUserPermissions();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/getUsers.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->getUsers(); // This will output the JSON response
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/removeUser.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->removeUser();
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->disableTOTP();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/totp_recover.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->recoverTOTP();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/totp_saveCode.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->saveTOTPRecoveryCode();
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->setupTOTP();
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->verifyTOTP();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/updateUserPanel.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->updateUserPanel();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/updateUserPermissions.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->updateUserPermissions();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/upload/removeChunks.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/uploadController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UploadController.php';
|
||||
|
||||
$uploadController = new UploadController();
|
||||
$uploadController->removeChunks();
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
// public/api/upload/upload.php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/uploadController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UploadController.php';
|
||||
|
||||
$uploadController = new UploadController();
|
||||
$uploadController->handleUpload();
|
||||
@@ -80,6 +80,9 @@ body.dark-mode .header-container {
|
||||
background-color: #1f1f1f;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
#darkModeIcon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
max-height: 50px;
|
||||
|
||||
@@ -5,24 +5,35 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title data-i18n-key="title">FileRise</title>
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('logout') === '1') {
|
||||
localStorage.removeItem("username");
|
||||
localStorage.removeItem("userTOTPEnabled");
|
||||
}
|
||||
</script>
|
||||
<link rel="icon" type="image/png" href="/assets/logo.png">
|
||||
<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 href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.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>
|
||||
@@ -41,9 +52,9 @@
|
||||
<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>
|
||||
<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" />
|
||||
</head>
|
||||
|
||||
@@ -78,16 +89,16 @@
|
||||
stroke: white;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
|
||||
.divider {
|
||||
stroke: #1565C0;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
|
||||
.drawer {
|
||||
fill: #FFFFFF;
|
||||
}
|
||||
|
||||
|
||||
.handle {
|
||||
fill: #1565C0;
|
||||
}
|
||||
@@ -159,16 +170,52 @@
|
||||
<button id="removeUserBtn" data-i18n-title="remove_user" style="display: none;">
|
||||
<i class="material-icons">person_remove</i>
|
||||
</button>
|
||||
<button id="darkModeToggle" class="dark-mode-toggle" data-i18n-key="dark_mode_toggle">Dark Mode</button>
|
||||
<button id="darkModeToggle" class="btn-icon" aria-label="Toggle dark mode">
|
||||
<span class="material-icons" id="darkModeIcon">
|
||||
dark_mode
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="loadingOverlay"></div>
|
||||
|
||||
<!-- Custom Toast Container -->
|
||||
<div id="customToast"></div>
|
||||
<div id="hiddenCardsContainer" style="display:none;"></div>
|
||||
|
||||
<div class="row mt-4" id="loginForm">
|
||||
<div class="col-12">
|
||||
<form id="authForm" method="post">
|
||||
<div class="form-group">
|
||||
<label for="loginUsername" data-i18n-key="user">User:</label>
|
||||
<input type="text" class="form-control" id="loginUsername" name="username" required autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword" data-i18n-key="password">Password:</label>
|
||||
<input type="password" class="form-control" id="loginPassword" name="password" required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block btn-login" data-i18n-key="login">Login</button>
|
||||
<div class="form-group remember-me-container">
|
||||
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
|
||||
<label for="rememberMeCheckbox" data-i18n-key="remember_me">Remember me</label>
|
||||
</div>
|
||||
</form>
|
||||
<!-- OIDC Login Option -->
|
||||
<div class="text-center mt-3">
|
||||
<button id="oidcLoginBtn" class="btn btn-secondary" data-i18n-key="login_oidc">Login with OIDC</button>
|
||||
</div>
|
||||
<!-- Basic HTTP Login Option -->
|
||||
<div class="text-center mt-3">
|
||||
<a href="/api/auth/login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic
|
||||
HTTP
|
||||
Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Wrapper: Hidden by default; remove "display: none;" after login -->
|
||||
<div class="main-wrapper">
|
||||
<!-- Sidebar Drop Zone: Hidden until you drag a card (display controlled by JS) -->
|
||||
@@ -176,36 +223,6 @@
|
||||
<!-- Main Column -->
|
||||
<div id="mainColumn" class="main-column">
|
||||
<div class="container-fluid">
|
||||
<!-- Login Form (unchanged) -->
|
||||
<div class="row" id="loginForm">
|
||||
<div class="col-12">
|
||||
<form id="authForm" method="post">
|
||||
<div class="form-group">
|
||||
<label for="loginUsername" data-i18n-key="user">User:</label>
|
||||
<input type="text" class="form-control" id="loginUsername" name="username" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword" data-i18n-key="password">Password:</label>
|
||||
<input type="password" class="form-control" id="loginPassword" name="password" required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block btn-login" data-i18n-key="login">Login</button>
|
||||
<div class="form-group remember-me-container">
|
||||
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
|
||||
<label for="rememberMeCheckbox" data-i18n-key="remember_me">Remember me</label>
|
||||
</div>
|
||||
</form>
|
||||
<!-- OIDC Login Option -->
|
||||
<div class="text-center mt-3">
|
||||
<button id="oidcLoginBtn" class="btn btn-secondary" data-i18n-key="login_oidc">Login with OIDC</button>
|
||||
</div>
|
||||
<!-- Basic HTTP Login Option -->
|
||||
<div class="text-center mt-3">
|
||||
<a href="/api/auth/login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic HTTP
|
||||
Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Operations: Upload and Folder Management -->
|
||||
<div id="mainOperations">
|
||||
<div class="container" style="max-width: 1400px; margin: 0 auto;">
|
||||
@@ -284,10 +301,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button id="shareFolderBtn" class="btn btn-secondary ml-2" data-i18n-title="share_folder">
|
||||
<i class="material-icons">share</i>
|
||||
</button>
|
||||
</button>
|
||||
<button id="deleteFolderBtn" class="btn btn-danger ml-2" data-i18n-title="delete_folder">
|
||||
<i class="material-icons">delete</i>
|
||||
</button>
|
||||
@@ -391,36 +408,43 @@
|
||||
</div> <!-- end mainColumn -->
|
||||
</div> <!-- end main-wrapper -->
|
||||
|
||||
<!-- Download Progress Modal -->
|
||||
<div id="downloadProgressModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||
<!-- Material icon spinner with a dedicated class -->
|
||||
<span class="material-icons download-spinner">autorenew</span>
|
||||
<p data-i18n-key="preparing_download">Preparing your download...</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Download Progress Modal -->
|
||||
<div id="downloadProgressModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||
<h4 id="downloadProgressTitle" data-i18n-key="preparing_download">
|
||||
Preparing your download...
|
||||
</h4>
|
||||
|
||||
<!-- Single File Download Modal -->
|
||||
<div id="downloadFileModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||
<h4 data-i18n-key="download_file">Download File</h4>
|
||||
<p data-i18n-key="confirm_or_change_filename">Confirm or change the download file name:</p>
|
||||
<input type="text" id="downloadFileNameInput" class="form-control" data-i18n-placeholder="filename" placeholder="Filename" />
|
||||
<div style="margin-top: 15px; text-align: right;">
|
||||
<button id="cancelDownloadFile" class="btn btn-secondary"
|
||||
onclick="document.getElementById('downloadFileModal').style.display = 'none';"
|
||||
data-i18n-key="cancel">Cancel</button>
|
||||
<button id="confirmSingleDownloadButton" class="btn btn-primary"
|
||||
onclick="confirmSingleDownload()"
|
||||
data-i18n-key="download">Download</button>
|
||||
<!-- spinner -->
|
||||
<span class="material-icons download-spinner">autorenew</span>
|
||||
|
||||
<!-- these were missing -->
|
||||
<progress id="downloadProgressBar" value="0" max="100" style="width:100%; height:1.5em; display:none;"></progress>
|
||||
<p>
|
||||
<span id="downloadProgressPercent" style="display:none;">0%</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Single File Download Modal -->
|
||||
<div id="downloadFileModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||
<h4 data-i18n-key="download_file">Download File</h4>
|
||||
<p data-i18n-key="confirm_or_change_filename">Confirm or change the download file name:</p>
|
||||
<input type="text" id="downloadFileNameInput" class="form-control" data-i18n-placeholder="filename"
|
||||
placeholder="Filename" />
|
||||
<div style="margin-top: 15px; text-align: right;">
|
||||
<button id="cancelDownloadFile" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
|
||||
<button id="confirmSingleDownloadButton" class="btn btn-primary" data-i18n-key="download">Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
||||
<div id="changePasswordModal" class="modal" style="display:none;">
|
||||
<div class="modal-content" style="max-width:400px; margin:auto;">
|
||||
<span id="closeChangePasswordModal" style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">×</span>
|
||||
<span id="closeChangePasswordModal"
|
||||
class="editor-close-btn">×</span>
|
||||
<h3 data-i18n-key="change_password_title">Change Password</h3>
|
||||
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
|
||||
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
|
||||
@@ -431,24 +455,36 @@
|
||||
<button id="saveNewPasswordBtn" class="btn btn-primary" data-i18n-key="save" style="width:100%;">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="addUserModal" class="modal">
|
||||
<div id="addUserModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<h3 data-i18n-key="create_new_user_title">Create New User</h3>
|
||||
<label for="newUsername" data-i18n-key="username">Username:</label>
|
||||
<input type="text" id="newUsername" class="form-control" />
|
||||
<label for="addUserPassword" data-i18n-key="password">Password:</label>
|
||||
<input type="password" id="addUserPassword" class="form-control" />
|
||||
<div id="adminCheckboxContainer">
|
||||
<input type="checkbox" id="isAdmin" />
|
||||
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
|
||||
<button id="saveUserBtn" class="btn btn-primary" data-i18n-key="save_user">Save User</button>
|
||||
</div>
|
||||
<!-- 1) Add a form around these fields -->
|
||||
<form id="addUserForm">
|
||||
<label for="newUsername" data-i18n-key="username">Username:</label>
|
||||
<input type="text" id="newUsername" class="form-control" required />
|
||||
|
||||
<label for="addUserPassword" data-i18n-key="password">Password:</label>
|
||||
<input type="password" id="addUserPassword" class="form-control" required />
|
||||
|
||||
<div id="adminCheckboxContainer">
|
||||
<input type="checkbox" id="isAdmin" />
|
||||
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<!-- Cancel stays type="button" -->
|
||||
<button type="button" id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<!-- Save becomes type="submit" -->
|
||||
<button type="submit" id="saveUserBtn" class="btn btn-primary" data-i18n-key="save_user">
|
||||
Save User
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="removeUserModal" class="modal">
|
||||
<div id="removeUserModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<h3 data-i18n-key="remove_user_title">Remove User</h3>
|
||||
<label for="removeUsernameSelect" data-i18n-key="select_user_remove">Select a user to remove:</label>
|
||||
@@ -459,7 +495,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="renameFileModal" class="modal">
|
||||
<div id="renameFileModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<h4 data-i18n-key="rename_file_title">Rename File</h4>
|
||||
<input type="text" id="newFileName" class="form-control" data-i18n-placeholder="rename_file_placeholder"
|
||||
|
||||
672
public/js/adminPanel.js
Normal file
672
public/js/adminPanel.js
Normal file
@@ -0,0 +1,672 @@
|
||||
import { t } from './i18n.js';
|
||||
import { loadAdminConfigFunc } from './auth.js';
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
|
||||
const version = "v1.3.1";
|
||||
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
|
||||
|
||||
// ————— Inject updated styles —————
|
||||
(function () {
|
||||
if (document.getElementById('adminPanelStyles')) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = 'adminPanelStyles';
|
||||
style.textContent = `
|
||||
/* Modal sizing */
|
||||
#adminPanelModal .modal-content {
|
||||
max-width: 1100px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
/* Small phones: 90% width */
|
||||
@media (max-width: 900px) {
|
||||
#adminPanelModal .modal-content {
|
||||
width: 90% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark-mode fixes */
|
||||
body.dark-mode #adminPanelModal .modal-content {
|
||||
border-color: #555 !important;
|
||||
}
|
||||
|
||||
/* enforce light‐mode styling */
|
||||
#adminPanelModal .modal-content {
|
||||
max-width: 1100px;
|
||||
width: 50%;
|
||||
background: #fff !important;
|
||||
color: #000 !important;
|
||||
border: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
/* enforce dark‐mode styling */
|
||||
body.dark-mode #adminPanelModal .modal-content {
|
||||
background: #2c2c2c !important;
|
||||
color: #e0e0e0 !important;
|
||||
border-color: #555 !important;
|
||||
}
|
||||
|
||||
/* form controls in dark */
|
||||
body.dark-mode .form-control {
|
||||
background-color: #333;
|
||||
border-color: #555;
|
||||
color: #eee;
|
||||
}
|
||||
body.dark-mode .form-control::placeholder { color: #888; }
|
||||
|
||||
/* Section headers */
|
||||
.section-header {
|
||||
background: #f5f5f5;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.section-header:first-of-type { margin-top: 0; }
|
||||
.section-header.collapsed .material-icons { transform: rotate(-90deg); }
|
||||
.section-header .material-icons { transition: transform .3s; color: #444; }
|
||||
|
||||
body.dark-mode .section-header {
|
||||
background: #3a3a3a;
|
||||
color: #eee;
|
||||
}
|
||||
body.dark-mode .section-header .material-icons { color: #ccc; }
|
||||
|
||||
/* Hidden by default */
|
||||
.section-content {
|
||||
display: none;
|
||||
margin-left: 20px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Close button */
|
||||
#adminPanelModal .editor-close-btn {
|
||||
position: absolute; top:10px; right:10px;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
font-size:20px; font-weight:bold; cursor:pointer;
|
||||
z-index:1000; width:32px; height:32px; border-radius:50%;
|
||||
text-align:center; line-height:30px;
|
||||
color:#ff4d4d; background:rgba(255,255,255,0.9);
|
||||
border:2px solid transparent; transition:all .3s;
|
||||
}
|
||||
#adminPanelModal .editor-close-btn:hover {
|
||||
color:white; background:#ff4d4d;
|
||||
box-shadow:0 0 6px rgba(255,77,77,.8);
|
||||
transform:scale(1.05);
|
||||
}
|
||||
body.dark-mode #adminPanelModal .editor-close-btn {
|
||||
background:rgba(0,0,0,0.6);
|
||||
color:#ff4d4d;
|
||||
}
|
||||
|
||||
/* Action-row */
|
||||
.action-row {
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
margin-top:15px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
})();
|
||||
// ————————————————————————————————————
|
||||
|
||||
let originalAdminConfig = {};
|
||||
function captureInitialAdminConfig() {
|
||||
originalAdminConfig = {
|
||||
headerTitle: document.getElementById("headerTitle").value.trim(),
|
||||
oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||
oidcClientId: document.getElementById("oidcClientId").value.trim(),
|
||||
oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
||||
oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(),
|
||||
disableFormLogin: document.getElementById("disableFormLogin").checked,
|
||||
disableBasicAuth: document.getElementById("disableBasicAuth").checked,
|
||||
disableOIDCLogin: document.getElementById("disableOIDCLogin").checked,
|
||||
enableWebDAV: document.getElementById("enableWebDAV").checked,
|
||||
sharedMaxUploadSize: document.getElementById("sharedMaxUploadSize").value.trim(),
|
||||
globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim()
|
||||
};
|
||||
}
|
||||
function hasUnsavedChanges() {
|
||||
const o = originalAdminConfig;
|
||||
return (
|
||||
document.getElementById("headerTitle").value.trim() !== o.headerTitle ||
|
||||
document.getElementById("oidcProviderUrl").value.trim() !== o.oidcProviderUrl ||
|
||||
document.getElementById("oidcClientId").value.trim() !== o.oidcClientId ||
|
||||
document.getElementById("oidcClientSecret").value.trim() !== o.oidcClientSecret ||
|
||||
document.getElementById("oidcRedirectUri").value.trim() !== o.oidcRedirectUri ||
|
||||
document.getElementById("disableFormLogin").checked !== o.disableFormLogin ||
|
||||
document.getElementById("disableBasicAuth").checked !== o.disableBasicAuth ||
|
||||
document.getElementById("disableOIDCLogin").checked !== o.disableOIDCLogin ||
|
||||
document.getElementById("enableWebDAV").checked !== o.enableWebDAV ||
|
||||
document.getElementById("sharedMaxUploadSize").value.trim() !== o.sharedMaxUploadSize ||
|
||||
document.getElementById("globalOtpauthUrl").value.trim() !== o.globalOtpauthUrl
|
||||
);
|
||||
}
|
||||
|
||||
function showCustomConfirmModal(message) {
|
||||
return new Promise(resolve => {
|
||||
const modal = document.getElementById("customConfirmModal");
|
||||
const msg = document.getElementById("confirmMessage");
|
||||
const yes = document.getElementById("confirmYesBtn");
|
||||
const no = document.getElementById("confirmNoBtn");
|
||||
msg.textContent = message;
|
||||
modal.style.display = "block";
|
||||
function clean() {
|
||||
modal.style.display = "none";
|
||||
yes.removeEventListener("click", onYes);
|
||||
no.removeEventListener("click", onNo);
|
||||
}
|
||||
function onYes() { clean(); resolve(true); }
|
||||
function onNo() { clean(); resolve(false); }
|
||||
yes.addEventListener("click", onYes);
|
||||
no.addEventListener("click", onNo);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSection(id) {
|
||||
const hdr = document.getElementById(id + "Header");
|
||||
const cnt = document.getElementById(id + "Content");
|
||||
const isCollapsedNow = hdr.classList.toggle("collapsed");
|
||||
// collapsed class present => hide; absent => show
|
||||
cnt.style.display = isCollapsedNow ? "none" : "block";
|
||||
if (!isCollapsedNow && id === "shareLinks") {
|
||||
loadShareLinksSection();
|
||||
}
|
||||
}
|
||||
|
||||
function loadShareLinksSection() {
|
||||
const container = document.getElementById("shareLinksContent");
|
||||
container.textContent = t("loading") + "...";
|
||||
|
||||
// helper: fetch one metadata file, but never throw —
|
||||
// on non-2xx (including 404) or network error, resolve to {}
|
||||
function fetchMeta(fileName) {
|
||||
return fetch(`/api/admin/readMetadata.php?file=${encodeURIComponent(fileName)}`, {
|
||||
credentials: "include"
|
||||
})
|
||||
.then(resp => {
|
||||
if (!resp.ok) {
|
||||
// 404 or any other non-OK → treat as empty
|
||||
return {};
|
||||
}
|
||||
return resp.json();
|
||||
})
|
||||
.catch(() => {
|
||||
// network failure, parse error, etc → also empty
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
fetchMeta("share_folder_links.json"),
|
||||
fetchMeta("share_links.json")
|
||||
])
|
||||
.then(([folders, files]) => {
|
||||
// if *both* are empty, show "no shared links"
|
||||
const hasAny = Object.keys(folders).length || Object.keys(files).length;
|
||||
if (!hasAny) {
|
||||
container.innerHTML = `<p>${t("no_shared_links_available")}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<h5>${t("folder_shares")}</h5><ul>`;
|
||||
Object.entries(folders).forEach(([token, o]) => {
|
||||
const lock = o.password ? "🔒 " : "";
|
||||
html += `
|
||||
<li>
|
||||
${lock}<strong>${o.folder}</strong>
|
||||
<small>(${new Date(o.expires * 1000).toLocaleString()})</small>
|
||||
<button type="button"
|
||||
data-key="${token}"
|
||||
data-type="folder"
|
||||
class="btn btn-sm btn-link delete-share">🗑️</button>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
html += `</ul><h5 style="margin-top:1em;">${t("file_shares")}</h5><ul>`;
|
||||
Object.entries(files).forEach(([token, o]) => {
|
||||
const lock = o.password ? "🔒 " : "";
|
||||
html += `
|
||||
<li>
|
||||
${lock}<strong>${o.folder}/${o.file}</strong>
|
||||
<small>(${new Date(o.expires * 1000).toLocaleString()})</small>
|
||||
<button type="button"
|
||||
data-key="${token}"
|
||||
data-type="file"
|
||||
class="btn btn-sm btn-link delete-share">🗑️</button>
|
||||
</li>`;
|
||||
});
|
||||
html += `</ul>`;
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// wire up delete buttons
|
||||
container.querySelectorAll(".delete-share").forEach(btn => {
|
||||
btn.addEventListener("click", evt => {
|
||||
evt.preventDefault();
|
||||
const token = btn.dataset.key;
|
||||
const isFolder = btn.dataset.type === "folder";
|
||||
const endpoint = isFolder
|
||||
? "/api/folder/deleteShareFolderLink.php"
|
||||
: "/api/file/deleteShareLink.php";
|
||||
|
||||
fetch(endpoint, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ token })
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
})
|
||||
.then(json => {
|
||||
if (json.success) {
|
||||
showToast(t("share_deleted_successfully"));
|
||||
loadShareLinksSection();
|
||||
} else {
|
||||
showToast(t("error_deleting_share") + ": " + (json.error || ""), "error");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Delete error:", err);
|
||||
showToast(t("error_deleting_share"), "error");
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("loadShareLinksSection error:", err);
|
||||
container.textContent = t("error_loading_share_links");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function openAdminPanel() {
|
||||
fetch("/api/admin/getConfig.php", { credentials: "include" })
|
||||
.then(r => r.json())
|
||||
.then(config => {
|
||||
// apply header title + globals
|
||||
if (config.header_title) {
|
||||
document.querySelector(".header-title h1").textContent = config.header_title;
|
||||
window.headerTitle = config.header_title;
|
||||
}
|
||||
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
||||
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
||||
|
||||
const dark = document.body.classList.contains("dark-mode");
|
||||
const bg = dark ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||
const inner = `
|
||||
background:${dark ? "#2c2c2c" : "#fff"};
|
||||
color:${dark ? "#e0e0e0" : "#000"};
|
||||
padding:20px; max-width:1100px; width:50%;
|
||||
border-radius:8px; position:relative;
|
||||
max-height:90vh; overflow:auto;
|
||||
border:1px solid ${dark ? "#555" : "#ccc"};
|
||||
`;
|
||||
|
||||
let mdl = document.getElementById("adminPanelModal");
|
||||
if (!mdl) {
|
||||
mdl = document.createElement("div");
|
||||
mdl.id = "adminPanelModal";
|
||||
mdl.style.cssText = `
|
||||
position:fixed; top:0; left:0;
|
||||
width:100vw; height:100vh;
|
||||
background:${bg};
|
||||
display:flex; justify-content:center; align-items:center;
|
||||
z-index:3000;
|
||||
`;
|
||||
mdl.innerHTML = `
|
||||
<div class="modal-content" style="${inner}">
|
||||
<div class="editor-close-btn" id="closeAdminPanel">×</div>
|
||||
<h3>${adminTitle}</h3>
|
||||
<form id="adminPanelForm">
|
||||
|
||||
<!-- each section: header + content -->
|
||||
${[
|
||||
{ id: "userManagement", label: t("user_management") },
|
||||
{ id: "headerSettings", label: t("header_settings") },
|
||||
{ id: "loginOptions", label: t("login_options") },
|
||||
{ id: "webdav", label: "WebDAV Access" },
|
||||
{ id: "upload", label: t("shared_max_upload_size_bytes_title") },
|
||||
{ id: "oidc", label: t("oidc_configuration") + " & TOTP" },
|
||||
{ id: "shareLinks", label: t("manage_shared_links") }
|
||||
].map(sec => `
|
||||
<div id="${sec.id}Header" class="section-header collapsed">
|
||||
${sec.label} <i class="material-icons">expand_more</i>
|
||||
</div>
|
||||
<div id="${sec.id}Content" class="section-content"></div>
|
||||
`).join("")}
|
||||
|
||||
<div class="action-row">
|
||||
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">${t("cancel")}</button>
|
||||
<button type="button" id="saveAdminSettings" class="btn btn-primary">${t("save_settings")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(mdl);
|
||||
|
||||
// Bind close & cancel
|
||||
document.getElementById("closeAdminPanel")
|
||||
.addEventListener("click", closeAdminPanel);
|
||||
document.getElementById("cancelAdminSettings")
|
||||
.addEventListener("click", closeAdminPanel);
|
||||
|
||||
// Section toggles
|
||||
["userManagement", "headerSettings", "loginOptions", "webdav", "upload", "oidc", "shareLinks"]
|
||||
.forEach(id => {
|
||||
document.getElementById(id + "Header")
|
||||
.addEventListener("click", () => toggleSection(id));
|
||||
});
|
||||
|
||||
// Populate each section’s CONTENT:
|
||||
// — User Mgmt —
|
||||
document.getElementById("userManagementContent").innerHTML = `
|
||||
<button type="button" id="adminOpenAddUser" class="btn btn-success me-2">${t("add_user")}</button>
|
||||
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger me-2">${t("remove_user")}</button>
|
||||
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${t("user_permissions")}</button>
|
||||
`;
|
||||
document.getElementById("adminOpenAddUser")
|
||||
.addEventListener("click", () => {
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById("newUsername").focus();
|
||||
});
|
||||
document.getElementById("adminOpenRemoveUser")
|
||||
.addEventListener("click", () => {
|
||||
if (typeof window.loadUserList === "function") window.loadUserList();
|
||||
toggleVisibility("removeUserModal", true);
|
||||
});
|
||||
document.getElementById("adminOpenUserPermissions")
|
||||
.addEventListener("click", openUserPermissionsModal);
|
||||
|
||||
// — Header Settings —
|
||||
document.getElementById("headerSettingsContent").innerHTML = `
|
||||
<div class="form-group">
|
||||
<label for="headerTitle">${t("header_title_text")}:</label>
|
||||
<input type="text" id="headerTitle" class="form-control" value="${window.headerTitle}" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
// — Login Options —
|
||||
document.getElementById("loginOptionsContent").innerHTML = `
|
||||
<div class="form-group"><input type="checkbox" id="disableFormLogin" /> <label for="disableFormLogin">${t("disable_login_form")}</label></div>
|
||||
<div class="form-group"><input type="checkbox" id="disableBasicAuth" /> <label for="disableBasicAuth">${t("disable_basic_http_auth")}</label></div>
|
||||
<div class="form-group"><input type="checkbox" id="disableOIDCLogin" /> <label for="disableOIDCLogin">${t("disable_oidc_login")}</label></div>
|
||||
`;
|
||||
|
||||
// — WebDAV —
|
||||
document.getElementById("webdavContent").innerHTML = `
|
||||
<div class="form-group"><input type="checkbox" id="enableWebDAV" /> <label for="enableWebDAV">Enable WebDAV</label></div>
|
||||
`;
|
||||
|
||||
// — Upload —
|
||||
document.getElementById("uploadContent").innerHTML = `
|
||||
<div class="form-group">
|
||||
<label for="sharedMaxUploadSize">${t("shared_max_upload_size_bytes")}:</label>
|
||||
<input type="number" id="sharedMaxUploadSize" class="form-control" placeholder="e.g. 52428800" />
|
||||
<small>${t("max_bytes_shared_uploads_note")}</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// — OIDC & TOTP —
|
||||
document.getElementById("oidcContent").innerHTML = `
|
||||
<div class="form-group"><label for="oidcProviderUrl">${t("oidc_provider_url")}:</label><input type="text" id="oidcProviderUrl" class="form-control" value="${window.currentOIDCConfig.providerUrl}" /></div>
|
||||
<div class="form-group"><label for="oidcClientId">${t("oidc_client_id")}:</label><input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" /></div>
|
||||
<div class="form-group"><label for="oidcClientSecret">${t("oidc_client_secret")}:</label><input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" /></div>
|
||||
<div class="form-group"><label for="oidcRedirectUri">${t("oidc_redirect_uri")}:</label><input type="text" id="oidcRedirectUri" class="form-control" value="${window.currentOIDCConfig.redirectUri}" /></div>
|
||||
<div class="form-group"><label for="globalOtpauthUrl">${t("global_otpauth_url")}:</label><input type="text" id="globalOtpauthUrl" class="form-control" value="${window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" /></div>
|
||||
`;
|
||||
|
||||
// — Share Links —
|
||||
document.getElementById("shareLinksContent").textContent = t("loading") + "…";
|
||||
|
||||
// — Save handler & constraints —
|
||||
document.getElementById("saveAdminSettings")
|
||||
.addEventListener("click", handleSave);
|
||||
["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"].forEach(id => {
|
||||
document.getElementById(id)
|
||||
.addEventListener("change", e => {
|
||||
const chk = ["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"]
|
||||
.filter(i => document.getElementById(i).checked).length;
|
||||
if (chk === 3) {
|
||||
showToast(t("at_least_one_login_method"));
|
||||
e.target.checked = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize inputs from config + capture
|
||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||
document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
|
||||
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
||||
captureInitialAdminConfig();
|
||||
|
||||
} else {
|
||||
// modal already exists → just refresh values & re-show
|
||||
mdl.style.display = "flex";
|
||||
// update dark/light as above...
|
||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||
document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
|
||||
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
||||
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl;
|
||||
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
|
||||
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
|
||||
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
|
||||
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || '';
|
||||
captureInitialAdminConfig();
|
||||
}
|
||||
})
|
||||
.catch(() => {/* if even fetching fails, open empty panel */ });
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
const dFL = document.getElementById("disableFormLogin").checked;
|
||||
const dBA = document.getElementById("disableBasicAuth").checked;
|
||||
const dOIDC = document.getElementById("disableOIDCLogin").checked;
|
||||
const eWD = document.getElementById("enableWebDAV").checked;
|
||||
const sMax = parseInt(document.getElementById("sharedMaxUploadSize").value, 10) || 0;
|
||||
const nHT = document.getElementById("headerTitle").value.trim();
|
||||
const nOIDC = {
|
||||
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||
clientId: document.getElementById("oidcClientId").value.trim(),
|
||||
clientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
||||
redirectUri: document.getElementById("oidcRedirectUri").value.trim()
|
||||
};
|
||||
const gURL = document.getElementById("globalOtpauthUrl").value.trim();
|
||||
|
||||
if ([dFL, dBA, dOIDC].filter(x => x).length === 3) {
|
||||
showToast(t("at_least_one_login_method"));
|
||||
return;
|
||||
}
|
||||
|
||||
sendRequest("/api/admin/updateConfig.php", "POST", {
|
||||
header_title: nHT, oidc: nOIDC,
|
||||
disableFormLogin: dFL, disableBasicAuth: dBA, disableOIDCLogin: dOIDC,
|
||||
enableWebDAV: eWD, sharedMaxUploadSize: sMax, globalOtpauthUrl: gURL
|
||||
}, {
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
}).then(res => {
|
||||
if (res.success) {
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
captureInitialAdminConfig();
|
||||
closeAdminPanel();
|
||||
loadAdminConfigFunc();
|
||||
} else {
|
||||
showToast(t("error_updating_settings") + ": " + (res.error || t("unknown_error")), "error");
|
||||
}
|
||||
}).catch(() => {/*noop*/ });
|
||||
}
|
||||
|
||||
export async function closeAdminPanel() {
|
||||
if (hasUnsavedChanges()) {
|
||||
const ok = await showCustomConfirmModal(t("unsaved_changes_confirm"));
|
||||
if (!ok) return;
|
||||
}
|
||||
document.getElementById("adminPanelModal").style.display = "none";
|
||||
}
|
||||
|
||||
// --- New: User Permissions Modal ---
|
||||
export function openUserPermissionsModal() {
|
||||
let userPermissionsModal = document.getElementById("userPermissionsModal");
|
||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||
const modalContentStyles = `
|
||||
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
||||
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
||||
padding: 20px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
if (!userPermissionsModal) {
|
||||
userPermissionsModal = document.createElement("div");
|
||||
userPermissionsModal.id = "userPermissionsModal";
|
||||
userPermissionsModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: ${overlayBackground};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3500;
|
||||
`;
|
||||
userPermissionsModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeUserPermissionsModal" class="editor-close-btn">×</span>
|
||||
<h3>${t("user_permissions")}</h3>
|
||||
<div id="userPermissionsList" style="max-height: 300px; overflow-y: auto; margin-bottom: 15px;">
|
||||
<!-- User rows will be loaded here -->
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
||||
<button type="button" id="cancelUserPermissionsBtn" class="btn btn-secondary">${t("cancel")}</button>
|
||||
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">${t("save_permissions")}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(userPermissionsModal);
|
||||
document.getElementById("closeUserPermissionsModal").addEventListener("click", () => {
|
||||
userPermissionsModal.style.display = "none";
|
||||
});
|
||||
document.getElementById("cancelUserPermissionsBtn").addEventListener("click", () => {
|
||||
userPermissionsModal.style.display = "none";
|
||||
});
|
||||
document.getElementById("saveUserPermissionsBtn").addEventListener("click", () => {
|
||||
// Collect permissions data from each user row.
|
||||
const rows = userPermissionsModal.querySelectorAll(".user-permission-row");
|
||||
const permissionsData = [];
|
||||
rows.forEach(row => {
|
||||
const username = row.getAttribute("data-username");
|
||||
const folderOnlyCheckbox = row.querySelector("input[data-permission='folderOnly']");
|
||||
const readOnlyCheckbox = row.querySelector("input[data-permission='readOnly']");
|
||||
const disableUploadCheckbox = row.querySelector("input[data-permission='disableUpload']");
|
||||
permissionsData.push({
|
||||
username,
|
||||
folderOnly: folderOnlyCheckbox.checked,
|
||||
readOnly: readOnlyCheckbox.checked,
|
||||
disableUpload: disableUploadCheckbox.checked
|
||||
});
|
||||
});
|
||||
// Send the permissionsData to the server.
|
||||
sendRequest("/api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
showToast(t("user_permissions_updated_successfully"));
|
||||
userPermissionsModal.style.display = "none";
|
||||
} else {
|
||||
showToast(t("error_updating_permissions") + ": " + (response.error || t("unknown_error")));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast(t("error_updating_permissions"));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
userPermissionsModal.style.display = "flex";
|
||||
}
|
||||
// Load the list of users into the modal.
|
||||
loadUserPermissionsList();
|
||||
}
|
||||
|
||||
function loadUserPermissionsList() {
|
||||
const listContainer = document.getElementById("userPermissionsList");
|
||||
if (!listContainer) return;
|
||||
listContainer.innerHTML = "";
|
||||
|
||||
// First, fetch the current permissions from the server.
|
||||
fetch("/api/getUserPermissions.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(permissionsData => {
|
||||
// Then, fetch the list of users.
|
||||
return fetch("/api/getUsers.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(usersData => {
|
||||
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
|
||||
if (users.length === 0) {
|
||||
listContainer.innerHTML = "<p>" + t("no_users_found") + "</p>";
|
||||
return;
|
||||
}
|
||||
users.forEach(user => {
|
||||
// Skip admin users.
|
||||
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
|
||||
|
||||
// Use stored permissions if available; otherwise fall back to defaults.
|
||||
const defaultPerm = {
|
||||
folderOnly: false,
|
||||
readOnly: false,
|
||||
disableUpload: false,
|
||||
};
|
||||
|
||||
// Normalize the username key to match server storage (e.g., lowercase)
|
||||
const usernameKey = user.username.toLowerCase();
|
||||
|
||||
const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData))
|
||||
? permissionsData[usernameKey]
|
||||
: defaultPerm;
|
||||
|
||||
// Create a row for the user.
|
||||
const row = document.createElement("div");
|
||||
row.classList.add("user-permission-row");
|
||||
row.setAttribute("data-username", user.username);
|
||||
row.style.padding = "10px 0";
|
||||
row.innerHTML = `
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">${user.username}</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="folderOnly" ${userPerm.folderOnly ? "checked" : ""} />
|
||||
${t("user_folder_only")}
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="readOnly" ${userPerm.readOnly ? "checked" : ""} />
|
||||
${t("read_only")}
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="disableUpload" ${userPerm.disableUpload ? "checked" : ""} />
|
||||
${t("disable_upload")}
|
||||
</label>
|
||||
</div>
|
||||
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
|
||||
`;
|
||||
listContainer.appendChild(row);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";
|
||||
});
|
||||
}
|
||||
@@ -15,10 +15,10 @@ import {
|
||||
openUserPanel,
|
||||
openTOTPModal,
|
||||
closeTOTPModal,
|
||||
openAdminPanel,
|
||||
closeAdminPanel,
|
||||
setLastLoginData
|
||||
} from './authModals.js';
|
||||
import { openAdminPanel } from './adminPanel.js';
|
||||
import { initializeApp } from './main.js';
|
||||
|
||||
// Production OIDC configuration (override via API as needed)
|
||||
const currentOIDCConfig = {
|
||||
@@ -44,6 +44,55 @@ function showToast(msgKey) {
|
||||
}
|
||||
window.showToast = showToast;
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
|
||||
/*
|
||||
* @param {string} url
|
||||
* @param {object} options
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
export async function fetchWithCsrf(url, options = {}) {
|
||||
// 1) Merge in credentials + header
|
||||
options = {
|
||||
credentials: 'include',
|
||||
...options,
|
||||
};
|
||||
options.headers = {
|
||||
...(options.headers || {}),
|
||||
'X-CSRF-Token': window.csrfToken,
|
||||
};
|
||||
|
||||
// 2) First attempt
|
||||
let res = await originalFetch(url, options);
|
||||
|
||||
// 3) If we got a 403, try to refresh token & retry
|
||||
if (res.status === 403) {
|
||||
// 3a) See if the server gave us a new token header
|
||||
let newToken = res.headers.get('X-CSRF-Token');
|
||||
// 3b) Otherwise fall back to the /api/auth/token endpoint
|
||||
if (!newToken) {
|
||||
const tokRes = await originalFetch('/api/auth/token.php', { credentials: 'include' });
|
||||
if (tokRes.ok) {
|
||||
const body = await tokRes.json();
|
||||
newToken = body.csrf_token;
|
||||
}
|
||||
}
|
||||
if (newToken) {
|
||||
// 3c) Update global + meta
|
||||
window.csrfToken = newToken;
|
||||
const meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (meta) meta.content = newToken;
|
||||
|
||||
// 3d) Retry the original request with the new token
|
||||
options.headers['X-CSRF-Token'] = newToken;
|
||||
res = await originalFetch(url, options);
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Return the real Response—no body peeking here!
|
||||
return res;
|
||||
}
|
||||
|
||||
// wrap the TOTP modal opener to disable other login buttons only for Basic/OIDC flows
|
||||
function openTOTPLoginModal() {
|
||||
originalOpenTOTPLoginModal();
|
||||
@@ -76,10 +125,17 @@ function updateItemsPerPageSelect() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
|
||||
const authForm = document.getElementById("authForm");
|
||||
|
||||
if (authForm) authForm.style.display = disableFormLogin ? "none" : "block";
|
||||
if
|
||||
(authForm) {
|
||||
authForm.style.display = disableFormLogin ? "none" : "block";
|
||||
setTimeout(() => {
|
||||
const loginInput = document.getElementById('loginUsername');
|
||||
if (loginInput) loginInput.focus();
|
||||
}, 0);
|
||||
}
|
||||
const basicAuthLink = document.querySelector("a[href='/api/auth/login_basic.php']");
|
||||
if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
|
||||
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||
@@ -134,11 +190,16 @@ function insertAfter(newNode, referenceNode) {
|
||||
}
|
||||
|
||||
function updateAuthenticatedUI(data) {
|
||||
document.getElementById('loadingOverlay').remove();
|
||||
|
||||
// show the wrapper (so the login form can be visible)
|
||||
document.querySelector('.main-wrapper').style.display = '';
|
||||
document.getElementById('loginForm').style.display = 'none';
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", true);
|
||||
toggleVisibility("uploadFileForm", true);
|
||||
toggleVisibility("fileListContainer", true);
|
||||
attachEnterKeyListener("addUserModal", "saveUserBtn");
|
||||
//attachEnterKeyListener("addUserModal", "saveUserBtn");
|
||||
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
|
||||
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||
@@ -208,6 +269,7 @@ function updateAuthenticatedUI(data) {
|
||||
userPanelBtn.style.display = "block";
|
||||
}
|
||||
}
|
||||
initializeApp();
|
||||
applyTranslations();
|
||||
updateItemsPerPageSelect();
|
||||
updateLoginOptionsUIFromStorage();
|
||||
@@ -217,6 +279,11 @@ function checkAuthentication(showLoginToast = true) {
|
||||
return sendRequest("/api/auth/checkAuth.php")
|
||||
.then(data => {
|
||||
if (data.setup) {
|
||||
document.getElementById('loadingOverlay').remove();
|
||||
|
||||
// show the wrapper (so the login form can be visible)
|
||||
document.querySelector('.main-wrapper').style.display = '';
|
||||
document.getElementById('loginForm').style.display = 'none';
|
||||
window.setupMode = true;
|
||||
if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user.");
|
||||
toggleVisibility("loginForm", false);
|
||||
@@ -228,6 +295,8 @@ function checkAuthentication(showLoginToast = true) {
|
||||
}
|
||||
window.setupMode = false;
|
||||
if (data.authenticated) {
|
||||
|
||||
localStorage.setItem('isAdmin', data.isAdmin ? 'true' : 'false');
|
||||
localStorage.setItem("folderOnly", data.folderOnly);
|
||||
localStorage.setItem("readOnly", data.readOnly);
|
||||
localStorage.setItem("disableUpload", data.disableUpload);
|
||||
@@ -235,9 +304,18 @@ function checkAuthentication(showLoginToast = true) {
|
||||
if (typeof data.totp_enabled !== "undefined") {
|
||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||
}
|
||||
if (data.csrf_token) {
|
||||
window.csrfToken = data.csrf_token;
|
||||
document.querySelector('meta[name="csrf-token"]').content = data.csrf_token;
|
||||
}
|
||||
updateAuthenticatedUI(data);
|
||||
return data;
|
||||
} else {
|
||||
document.getElementById('loadingOverlay').remove();
|
||||
|
||||
// show the wrapper (so the login form can be visible)
|
||||
document.querySelector('.main-wrapper').style.display = '';
|
||||
document.getElementById('loginForm').style.display = '';
|
||||
if (showLoginToast) showToast("Please log in to continue.");
|
||||
toggleVisibility("loginForm", true);
|
||||
toggleVisibility("mainOperations", false);
|
||||
@@ -276,11 +354,11 @@ async function submitLogin(data) {
|
||||
try {
|
||||
const perm = await sendRequest("/api/getUserPermissions.php", "GET");
|
||||
if (perm && typeof perm === "object") {
|
||||
localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false");
|
||||
localStorage.setItem("readOnly", perm.readOnly ? "true" : "false");
|
||||
localStorage.setItem("disableUpload",perm.disableUpload? "true" : "false");
|
||||
localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false");
|
||||
localStorage.setItem("readOnly", perm.readOnly ? "true" : "false");
|
||||
localStorage.setItem("disableUpload", perm.disableUpload ? "true" : "false");
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
return window.location.reload();
|
||||
}
|
||||
|
||||
@@ -383,45 +461,54 @@ function initAuth() {
|
||||
submitLogin(formData);
|
||||
});
|
||||
}
|
||||
document.getElementById("logoutBtn").addEventListener("click", function () {
|
||||
fetch("/api/auth/logout.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
}).then(() => window.location.reload(true)).catch(() => { });
|
||||
});
|
||||
|
||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||
resetUserForm();
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById("newUsername").focus();
|
||||
});
|
||||
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
||||
|
||||
// remove your old saveUserBtn click-handler…
|
||||
|
||||
// instead:
|
||||
const addUserForm = document.getElementById("addUserForm");
|
||||
addUserForm.addEventListener("submit", function (e) {
|
||||
e.preventDefault(); // stop the browser from reloading the page
|
||||
|
||||
const newUsername = document.getElementById("newUsername").value.trim();
|
||||
const newPassword = document.getElementById("addUserPassword").value.trim();
|
||||
const isAdmin = document.getElementById("isAdmin").checked;
|
||||
|
||||
if (!newUsername || !newPassword) {
|
||||
showToast("Username and password are required!");
|
||||
return;
|
||||
}
|
||||
|
||||
let url = "/api/addUser.php";
|
||||
if (window.setupMode) url += "?setup=1";
|
||||
fetch(url, {
|
||||
|
||||
fetchWithCsrf(url, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("User added successfully!");
|
||||
closeAddUserModal();
|
||||
checkAuthentication(false);
|
||||
if (window.setupMode) {
|
||||
toggleVisibility("loginForm", true);
|
||||
}
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not add user"));
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
.catch(() => {
|
||||
showToast("Error: Could not add user");
|
||||
});
|
||||
});
|
||||
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
|
||||
|
||||
@@ -438,10 +525,10 @@ function initAuth() {
|
||||
}
|
||||
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
||||
if (!confirmed) return;
|
||||
fetch("/api/removeUser.php", {
|
||||
fetchWithCsrf("/api/removeUser.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: usernameToRemove })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -477,10 +564,10 @@ function initAuth() {
|
||||
return;
|
||||
}
|
||||
const data = { oldPassword, newPassword, confirmPassword };
|
||||
fetch("/api/changePassword.php", {
|
||||
fetchWithCsrf("/api/changePassword.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -3,8 +3,6 @@ import { sendRequest } from './networkUtils.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
import { loadAdminConfigFunc } from './auth.js';
|
||||
|
||||
const version = "v1.2.2"; // Update this version string as needed
|
||||
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||
|
||||
let lastLoginData = null;
|
||||
export function setLastLoginData(data) {
|
||||
@@ -32,7 +30,7 @@ export function openTOTPLoginModal() {
|
||||
`;
|
||||
totpLoginModal.innerHTML = `
|
||||
<div style="background: ${modalBg}; padding:20px; border-radius:8px; text-align:center; position:relative; color:${textColor};">
|
||||
<span id="closeTOTPLoginModal" style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">×</span>
|
||||
<span id="closeTOTPLoginModal" class="editor-close-btn">×</span>
|
||||
<div id="totpSection">
|
||||
<h3>${t("enter_totp_code")}</h3>
|
||||
<input type="text" id="totpLoginInput" maxlength="6"
|
||||
@@ -174,11 +172,13 @@ export function openUserPanel() {
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: fixed;
|
||||
overflow-y: auto;
|
||||
max-height: 400px !important;
|
||||
overflow-x: hidden;
|
||||
max-height: 383px !important;
|
||||
flex-shrink: 0 !important;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
||||
transform: none;
|
||||
box-sizing: border-box;
|
||||
transition: none;
|
||||
`;
|
||||
const savedLanguage = localStorage.getItem("language") || "en";
|
||||
@@ -188,19 +188,17 @@ export function openUserPanel() {
|
||||
userPanelModal.id = "userPanelModal";
|
||||
userPanelModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0; right: 0; bottom: 0; left: 0;
|
||||
background-color: ${overlayBackground};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3000;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
`;
|
||||
userPanelModal.innerHTML = `
|
||||
<div class="modal-content user-panel-content" style="${modalContentStyles}">
|
||||
<span id="closeUserPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<div class="modal-content user-panel-content" style="${modalContentStyles}">
|
||||
<span id="closeUserPanel" class="editor-close-btn">×</span>
|
||||
<h3>${t("user_panel")} (${username})</h3>
|
||||
|
||||
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">
|
||||
@@ -230,14 +228,39 @@ export function openUserPanel() {
|
||||
|
||||
<!-- New API Docs link -->
|
||||
<div style="margin-bottom: 15px;">
|
||||
<a href="api.html" target="_blank" class="btn btn-secondary">
|
||||
${t("api_docs") || "API Docs"}
|
||||
</a>
|
||||
<button type="button" id="openApiModalBtn" class="btn btn-secondary">
|
||||
${t("api_docs") || "API Docs"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(userPanelModal);
|
||||
|
||||
const apiModal = document.createElement("div");
|
||||
apiModal.id = "apiModal";
|
||||
apiModal.style.cssText = `
|
||||
position: fixed; top:0; left:0; width:100vw; height:100vh;
|
||||
background: rgba(0,0,0,0.8); z-index: 4000; display:none;
|
||||
align-items: center; justify-content: center;
|
||||
`;
|
||||
|
||||
// api.php
|
||||
apiModal.innerHTML = `
|
||||
<div style="position:relative; width:90vw; height:90vh; background:#fff; border-radius:8px; overflow:hidden;">
|
||||
<div class="editor-close-btn" id="closeApiModal">×</div>
|
||||
<iframe src="api.php" style="width:100%;height:100%;border:none;"></iframe>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(apiModal);
|
||||
|
||||
document.getElementById("openApiModalBtn").addEventListener("click", () => {
|
||||
apiModal.style.display = "flex";
|
||||
});
|
||||
document.getElementById("closeApiModal").addEventListener("click", () => {
|
||||
apiModal.style.display = "none";
|
||||
});
|
||||
|
||||
// Handlers…
|
||||
document.getElementById("closeUserPanel").addEventListener("click", () => {
|
||||
userPanelModal.style.display = "none";
|
||||
@@ -246,6 +269,7 @@ export function openUserPanel() {
|
||||
document.getElementById("changePasswordModal").style.display = "block";
|
||||
});
|
||||
|
||||
|
||||
// TOTP checkbox
|
||||
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
||||
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
|
||||
@@ -345,7 +369,7 @@ export function openTOTPModal() {
|
||||
`;
|
||||
totpModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<span id="closeTOTPModal" class="editor-close-btn">×</span>
|
||||
<h3>${t("totp_setup")}</h3>
|
||||
<p>${t("scan_qr_code")}</p>
|
||||
<!-- Create an image placeholder without the CSRF token in the src -->
|
||||
@@ -518,495 +542,4 @@ export function closeTOTPModal(disable = true) {
|
||||
})
|
||||
.catch(() => { showToast(t("error_disabling_totp_setting")); });
|
||||
}
|
||||
}
|
||||
|
||||
// Global variable to hold the initial state of the admin form.
|
||||
let originalAdminConfig = {};
|
||||
|
||||
// Capture the initial state of the admin form fields.
|
||||
function captureInitialAdminConfig() {
|
||||
originalAdminConfig = {
|
||||
headerTitle: document.getElementById("headerTitle").value.trim(),
|
||||
oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||
oidcClientId: document.getElementById("oidcClientId").value.trim(),
|
||||
oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
||||
oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(),
|
||||
disableFormLogin: document.getElementById("disableFormLogin").checked,
|
||||
disableBasicAuth: document.getElementById("disableBasicAuth").checked,
|
||||
disableOIDCLogin: document.getElementById("disableOIDCLogin").checked,
|
||||
globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim()
|
||||
};
|
||||
}
|
||||
|
||||
// Compare current values to the captured initial state.
|
||||
function hasUnsavedChanges() {
|
||||
return (
|
||||
document.getElementById("headerTitle").value.trim() !== originalAdminConfig.headerTitle ||
|
||||
document.getElementById("oidcProviderUrl").value.trim() !== originalAdminConfig.oidcProviderUrl ||
|
||||
document.getElementById("oidcClientId").value.trim() !== originalAdminConfig.oidcClientId ||
|
||||
document.getElementById("oidcClientSecret").value.trim() !== originalAdminConfig.oidcClientSecret ||
|
||||
document.getElementById("oidcRedirectUri").value.trim() !== originalAdminConfig.oidcRedirectUri ||
|
||||
document.getElementById("disableFormLogin").checked !== originalAdminConfig.disableFormLogin ||
|
||||
document.getElementById("disableBasicAuth").checked !== originalAdminConfig.disableBasicAuth ||
|
||||
document.getElementById("disableOIDCLogin").checked !== originalAdminConfig.disableOIDCLogin ||
|
||||
document.getElementById("globalOtpauthUrl").value.trim() !== originalAdminConfig.globalOtpauthUrl
|
||||
);
|
||||
}
|
||||
|
||||
// Use your custom confirmation modal.
|
||||
function showCustomConfirmModal(message) {
|
||||
return new Promise((resolve) => {
|
||||
// Get modal elements from DOM.
|
||||
const modal = document.getElementById("customConfirmModal");
|
||||
const messageElem = document.getElementById("confirmMessage");
|
||||
const yesBtn = document.getElementById("confirmYesBtn");
|
||||
const noBtn = document.getElementById("confirmNoBtn");
|
||||
|
||||
// Set the message in the modal.
|
||||
messageElem.textContent = message;
|
||||
modal.style.display = "block";
|
||||
|
||||
// Define event handlers.
|
||||
function onYes() {
|
||||
cleanup();
|
||||
resolve(true);
|
||||
}
|
||||
function onNo() {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}
|
||||
// Remove event listeners and hide modal after choice.
|
||||
function cleanup() {
|
||||
yesBtn.removeEventListener("click", onYes);
|
||||
noBtn.removeEventListener("click", onNo);
|
||||
modal.style.display = "none";
|
||||
}
|
||||
|
||||
yesBtn.addEventListener("click", onYes);
|
||||
noBtn.addEventListener("click", onNo);
|
||||
});
|
||||
}
|
||||
|
||||
export function openAdminPanel() {
|
||||
fetch("/api/admin/getConfig.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
if (config.header_title) {
|
||||
document.querySelector(".header-title h1").textContent = config.header_title;
|
||||
window.headerTitle = config.header_title || "FileRise";
|
||||
}
|
||||
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
||||
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||
const modalContentStyles = `
|
||||
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
||||
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
max-height: 90vh;
|
||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
||||
`;
|
||||
let adminModal = document.getElementById("adminPanelModal");
|
||||
|
||||
if (!adminModal) {
|
||||
adminModal = document.createElement("div");
|
||||
adminModal.id = "adminPanelModal";
|
||||
adminModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: ${overlayBackground};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3000;
|
||||
`;
|
||||
adminModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeAdminPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>${adminTitle}</h3>
|
||||
<form id="adminPanelForm">
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>${t("user_management")}</legend>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button type="button" id="adminOpenAddUser" class="btn btn-success">${t("add_user")}</button>
|
||||
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">${t("remove_user")}</button>
|
||||
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${t("user_permissions")}</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>Header Settings</legend>
|
||||
<div class="form-group">
|
||||
<label for="headerTitle">Header Title:</label>
|
||||
<input type="text" id="headerTitle" class="form-control" value="${window.headerTitle}" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>${t("login_options")}</legend>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableFormLogin" />
|
||||
<label for="disableFormLogin">${t("disable_login_form")}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableBasicAuth" />
|
||||
<label for="disableBasicAuth">${t("disable_basic_http_auth")}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableOIDCLogin" />
|
||||
<label for="disableOIDCLogin">${t("disable_oidc_login")}</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>${t("oidc_configuration")}</legend>
|
||||
<div class="form-group">
|
||||
<label for="oidcProviderUrl">${t("oidc_provider_url")}:</label>
|
||||
<input type="text" id="oidcProviderUrl" class="form-control" value="${window.currentOIDCConfig.providerUrl}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcClientId">${t("oidc_client_id")}:</label>
|
||||
<input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcClientSecret">${t("oidc_client_secret")}:</label>
|
||||
<input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcRedirectUri">${t("oidc_redirect_uri")}:</label>
|
||||
<input type="text" id="oidcRedirectUri" class="form-control" value="${window.currentOIDCConfig.redirectUri}" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>${t("global_totp_settings")}</legend>
|
||||
<div class="form-group">
|
||||
<label for="globalOtpauthUrl">${t("global_otpauth_url")}:</label>
|
||||
<input type="text" id="globalOtpauthUrl" class="form-control" value="${window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">${t("cancel")}</button>
|
||||
<button type="button" id="saveAdminSettings" class="btn btn-primary">${t("save_settings")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(adminModal);
|
||||
|
||||
// Bind closing events that will use our enhanced close function.
|
||||
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
||||
adminModal.addEventListener("click", (e) => {
|
||||
if (e.target === adminModal) closeAdminPanel();
|
||||
});
|
||||
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
||||
|
||||
// Bind other buttons.
|
||||
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById("newUsername").focus();
|
||||
});
|
||||
document.getElementById("adminOpenRemoveUser").addEventListener("click", () => {
|
||||
if (typeof window.loadUserList === "function") {
|
||||
window.loadUserList();
|
||||
}
|
||||
toggleVisibility("removeUserModal", true);
|
||||
});
|
||||
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
|
||||
openUserPermissionsModal();
|
||||
});
|
||||
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
||||
|
||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
||||
if (totalDisabled === 3) {
|
||||
showToast(t("at_least_one_login_method"));
|
||||
disableOIDCLoginCheckbox.checked = false;
|
||||
localStorage.setItem("disableOIDCLogin", "false");
|
||||
if (typeof window.updateLoginOptionsUI === "function") {
|
||||
window.updateLoginOptionsUI({
|
||||
disableFormLogin: disableFormLoginCheckbox.checked,
|
||||
disableBasicAuth: disableBasicAuthCheckbox.checked,
|
||||
disableOIDCLogin: disableOIDCLoginCheckbox.checked
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const newHeaderTitle = document.getElementById("headerTitle").value.trim();
|
||||
|
||||
const newOIDCConfig = {
|
||||
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||
clientId: document.getElementById("oidcClientId").value.trim(),
|
||||
clientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
||||
redirectUri: document.getElementById("oidcRedirectUri").value.trim()
|
||||
};
|
||||
const disableFormLogin = disableFormLoginCheckbox.checked;
|
||||
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
||||
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
||||
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
||||
sendRequest("/api/admin/updateConfig.php", "POST", {
|
||||
header_title: newHeaderTitle,
|
||||
oidc: newOIDCConfig,
|
||||
disableFormLogin,
|
||||
disableBasicAuth,
|
||||
disableOIDCLogin,
|
||||
globalOtpauthUrl
|
||||
}, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
showToast(t("settings_updated_successfully"));
|
||||
localStorage.setItem("disableFormLogin", disableFormLogin);
|
||||
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
||||
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
||||
if (typeof window.updateLoginOptionsUI === "function") {
|
||||
window.updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
||||
}
|
||||
// Update the captured initial state since the changes have now been saved.
|
||||
captureInitialAdminConfig();
|
||||
closeAdminPanel();
|
||||
loadAdminConfigFunc();
|
||||
|
||||
} else {
|
||||
showToast(t("error_updating_settings") + ": " + (response.error || t("unknown_error")));
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
});
|
||||
// Enforce login option constraints.
|
||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||
function enforceLoginOptionConstraint(changedCheckbox) {
|
||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
||||
if (changedCheckbox.checked && totalDisabled === 3) {
|
||||
showToast(t("at_least_one_login_method"));
|
||||
changedCheckbox.checked = false;
|
||||
}
|
||||
}
|
||||
disableFormLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||
disableBasicAuthCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||
disableOIDCLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||
|
||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||
|
||||
// Capture initial state after the modal loads.
|
||||
captureInitialAdminConfig();
|
||||
} else {
|
||||
adminModal.style.backgroundColor = overlayBackground;
|
||||
const modalContent = adminModal.querySelector(".modal-content");
|
||||
if (modalContent) {
|
||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
||||
modalContent.style.border = isDarkMode ? "1px solid #444" : "1px solid #ccc";
|
||||
}
|
||||
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl;
|
||||
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
|
||||
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
|
||||
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
|
||||
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise';
|
||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||
adminModal.style.display = "flex";
|
||||
captureInitialAdminConfig();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
let adminModal = document.getElementById("adminPanelModal");
|
||||
if (adminModal) {
|
||||
adminModal.style.backgroundColor = "rgba(0,0,0,0.5)";
|
||||
const modalContent = adminModal.querySelector(".modal-content");
|
||||
if (modalContent) {
|
||||
modalContent.style.background = "#fff";
|
||||
modalContent.style.color = "#000";
|
||||
modalContent.style.border = "1px solid #ccc";
|
||||
}
|
||||
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl;
|
||||
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
|
||||
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
|
||||
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
|
||||
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise';
|
||||
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
||||
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
||||
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
||||
adminModal.style.display = "flex";
|
||||
captureInitialAdminConfig();
|
||||
} else {
|
||||
openAdminPanel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function closeAdminPanel() {
|
||||
if (hasUnsavedChanges()) {
|
||||
const userConfirmed = await showCustomConfirmModal(t("unsaved_changes_confirm"));
|
||||
if (!userConfirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const adminModal = document.getElementById("adminPanelModal");
|
||||
if (adminModal) adminModal.style.display = "none";
|
||||
}
|
||||
|
||||
// --- New: User Permissions Modal ---
|
||||
export function openUserPermissionsModal() {
|
||||
let userPermissionsModal = document.getElementById("userPermissionsModal");
|
||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||
const modalContentStyles = `
|
||||
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
||||
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
||||
padding: 20px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
if (!userPermissionsModal) {
|
||||
userPermissionsModal = document.createElement("div");
|
||||
userPermissionsModal.id = "userPermissionsModal";
|
||||
userPermissionsModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: ${overlayBackground};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3500;
|
||||
`;
|
||||
userPermissionsModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeUserPermissionsModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>${t("user_permissions")}</h3>
|
||||
<div id="userPermissionsList" style="max-height: 300px; overflow-y: auto; margin-bottom: 15px;">
|
||||
<!-- User rows will be loaded here -->
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
||||
<button type="button" id="cancelUserPermissionsBtn" class="btn btn-secondary">${t("cancel")}</button>
|
||||
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">${t("save_permissions")}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(userPermissionsModal);
|
||||
document.getElementById("closeUserPermissionsModal").addEventListener("click", () => {
|
||||
userPermissionsModal.style.display = "none";
|
||||
});
|
||||
document.getElementById("cancelUserPermissionsBtn").addEventListener("click", () => {
|
||||
userPermissionsModal.style.display = "none";
|
||||
});
|
||||
document.getElementById("saveUserPermissionsBtn").addEventListener("click", () => {
|
||||
// Collect permissions data from each user row.
|
||||
const rows = userPermissionsModal.querySelectorAll(".user-permission-row");
|
||||
const permissionsData = [];
|
||||
rows.forEach(row => {
|
||||
const username = row.getAttribute("data-username");
|
||||
const folderOnlyCheckbox = row.querySelector("input[data-permission='folderOnly']");
|
||||
const readOnlyCheckbox = row.querySelector("input[data-permission='readOnly']");
|
||||
const disableUploadCheckbox = row.querySelector("input[data-permission='disableUpload']");
|
||||
permissionsData.push({
|
||||
username,
|
||||
folderOnly: folderOnlyCheckbox.checked,
|
||||
readOnly: readOnlyCheckbox.checked,
|
||||
disableUpload: disableUploadCheckbox.checked
|
||||
});
|
||||
});
|
||||
// Send the permissionsData to the server.
|
||||
sendRequest("/api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
showToast(t("user_permissions_updated_successfully"));
|
||||
userPermissionsModal.style.display = "none";
|
||||
} else {
|
||||
showToast(t("error_updating_permissions") + ": " + (response.error || t("unknown_error")));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast(t("error_updating_permissions"));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
userPermissionsModal.style.display = "flex";
|
||||
}
|
||||
// Load the list of users into the modal.
|
||||
loadUserPermissionsList();
|
||||
}
|
||||
|
||||
function loadUserPermissionsList() {
|
||||
const listContainer = document.getElementById("userPermissionsList");
|
||||
if (!listContainer) return;
|
||||
listContainer.innerHTML = "";
|
||||
|
||||
// First, fetch the current permissions from the server.
|
||||
fetch("/api/getUserPermissions.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(permissionsData => {
|
||||
// Then, fetch the list of users.
|
||||
return fetch("/api/getUsers.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(usersData => {
|
||||
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
|
||||
if (users.length === 0) {
|
||||
listContainer.innerHTML = "<p>" + t("no_users_found") + "</p>";
|
||||
return;
|
||||
}
|
||||
users.forEach(user => {
|
||||
// Skip admin users.
|
||||
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
|
||||
|
||||
// Use stored permissions if available; otherwise fall back to defaults.
|
||||
const defaultPerm = {
|
||||
folderOnly: false,
|
||||
readOnly: false,
|
||||
disableUpload: false,
|
||||
};
|
||||
|
||||
// Normalize the username key to match server storage (e.g., lowercase)
|
||||
const usernameKey = user.username.toLowerCase();
|
||||
|
||||
const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData))
|
||||
? permissionsData[usernameKey]
|
||||
: defaultPerm;
|
||||
|
||||
// Create a row for the user.
|
||||
const row = document.createElement("div");
|
||||
row.classList.add("user-permission-row");
|
||||
row.setAttribute("data-username", user.username);
|
||||
row.style.padding = "10px 0";
|
||||
row.innerHTML = `
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">${user.username}</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="folderOnly" ${userPerm.folderOnly ? "checked" : ""} />
|
||||
${t("user_folder_only")}
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="readOnly" ${userPerm.readOnly ? "checked" : ""} />
|
||||
${t("read_only")}
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="disableUpload" ${userPerm.disableUpload ? "checked" : ""} />
|
||||
${t("disable_upload")}
|
||||
</label>
|
||||
</div>
|
||||
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
|
||||
`;
|
||||
listContainer.appendChild(row);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";
|
||||
});
|
||||
}
|
||||
@@ -25,8 +25,9 @@ export function toggleAllCheckboxes(masterCheckbox) {
|
||||
const checkboxes = document.querySelectorAll(".file-checkbox");
|
||||
checkboxes.forEach(chk => {
|
||||
chk.checked = masterCheckbox.checked;
|
||||
updateRowHighlight(chk);
|
||||
});
|
||||
updateFileActionButtons(); // update buttons based on current selection
|
||||
updateFileActionButtons();
|
||||
}
|
||||
|
||||
export function updateFileActionButtons() {
|
||||
@@ -38,6 +39,21 @@ export function updateFileActionButtons() {
|
||||
const zipBtn = document.getElementById("downloadZipBtn");
|
||||
const extractZipBtn = document.getElementById("extractZipBtn");
|
||||
|
||||
// keep the “select all” in sync ——
|
||||
const master = document.getElementById("selectAll");
|
||||
if (master) {
|
||||
if (selectedCheckboxes.length === fileCheckboxes.length) {
|
||||
master.checked = true;
|
||||
master.indeterminate = false;
|
||||
} else if (selectedCheckboxes.length === 0) {
|
||||
master.checked = false;
|
||||
master.indeterminate = false;
|
||||
} else {
|
||||
master.checked = false;
|
||||
master.indeterminate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (fileCheckboxes.length === 0) {
|
||||
if (copyBtn) copyBtn.style.display = "none";
|
||||
if (moveBtn) moveBtn.style.display = "none";
|
||||
@@ -91,7 +107,7 @@ export function showToast(message, duration = 3000) {
|
||||
export function buildSearchAndPaginationControls({ currentPage, totalPages, searchTerm }) {
|
||||
const safeSearchTerm = escapeHTML(searchTerm);
|
||||
// Choose the placeholder text based on advanced search mode
|
||||
const placeholderText = window.advancedSearchEnabled
|
||||
const placeholderText = window.advancedSearchEnabled
|
||||
? t("search_placeholder_advanced")
|
||||
: t("search_placeholder");
|
||||
|
||||
@@ -101,7 +117,7 @@ export function buildSearchAndPaginationControls({ currentPage, totalPages, sear
|
||||
<div class="input-group">
|
||||
<!-- Advanced Search Toggle Button -->
|
||||
<div class="input-group-prepend">
|
||||
<button id="advancedSearchToggle" class="btn btn-outline-secondary btn-icon" onclick="toggleAdvancedSearch()" title="${window.advancedSearchEnabled ? t("basic_search_tooltip") : t("advanced_search_tooltip")}">
|
||||
<button id="advancedSearchToggle" class="btn btn-outline-secondary btn-icon" title="${window.advancedSearchEnabled ? t("basic_search_tooltip") : t("advanced_search_tooltip")}">
|
||||
<i class="material-icons">${window.advancedSearchEnabled ? "filter_alt_off" : "filter_alt"}</i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -117,9 +133,9 @@ export function buildSearchAndPaginationControls({ currentPage, totalPages, sear
|
||||
</div>
|
||||
<div class="col-12 col-md-4 text-left">
|
||||
<div class="d-flex justify-content-center justify-content-md-start align-items-center">
|
||||
<button class="custom-prev-next-btn" ${currentPage === 1 ? "disabled" : ""} onclick="changePage(${currentPage - 1})">${t("prev")}</button>
|
||||
<button id="prevPageBtn" class="custom-prev-next-btn" ${currentPage === 1 ? "disabled" : ""}>${t("prev")}</button>
|
||||
<span class="page-indicator">${t("page")} ${currentPage} ${t("of")} ${totalPages || 1}</span>
|
||||
<button class="custom-prev-next-btn" ${currentPage === totalPages ? "disabled" : ""} onclick="changePage(${currentPage + 1})">${t("next")}</button>
|
||||
<button id="nextPageBtn" class="custom-prev-next-btn" ${currentPage === totalPages ? "disabled" : ""}>${t("next")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,7 +147,7 @@ export function buildFileTableHeader(sortOrder) {
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="checkbox-col"><input type="checkbox" id="selectAll" onclick="toggleAllCheckboxes(this)"></th>
|
||||
<th class="checkbox-col"><input type="checkbox" id="selectAll"></th>
|
||||
<th data-column="name" class="sortable-col">${t("file_name")} ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
|
||||
<th data-column="modified" class="hide-small sortable-col">${t("date_modified")} ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
|
||||
<th data-column="uploaded" class="hide-small hide-medium sortable-col">${t("upload_date")} ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
|
||||
@@ -162,15 +178,15 @@ export function buildFileTableRow(file, folderPath) {
|
||||
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||
previewIcon = `<i class="material-icons">audiotrack</i>`;
|
||||
}
|
||||
previewButton = `<button class="btn btn-sm btn-info preview-btn" onclick="event.stopPropagation(); previewFile('${folderPath + encodeURIComponent(file.name)}', '${safeFileName}')">
|
||||
previewButton = `<button class="btn btn-sm btn-info preview-btn" data-preview-url="${folderPath + encodeURIComponent(file.name)}?t=${Date.now()}" data-preview-name="${safeFileName}">
|
||||
${previewIcon}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<tr onclick="toggleRowSelection(event, '${safeFileName}')" class="clickable-row">
|
||||
<tr class="clickable-row">
|
||||
<td>
|
||||
<input type="checkbox" class="file-checkbox" value="${safeFileName}" onclick="event.stopPropagation(); updateRowHighlight(this);">
|
||||
<input type="checkbox" class="file-checkbox" value="${safeFileName}">
|
||||
</td>
|
||||
<td class="file-name-cell">${safeFileName}</td>
|
||||
<td class="hide-small nowrap">${safeModified}</td>
|
||||
@@ -179,22 +195,16 @@ export function buildFileTableRow(file, folderPath) {
|
||||
<td class="hide-small hide-medium nowrap">${safeUploader}</td>
|
||||
<td>
|
||||
<div class="button-wrap" style="display: flex; justify-content: left; gap: 5px;">
|
||||
<button type="button" class="btn btn-sm btn-success download-btn"
|
||||
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
|
||||
title="${t('download')}">
|
||||
<button type="button" class="btn btn-sm btn-success download-btn" data-download-name="${file.name}" data-download-folder="${file.folder || 'root'}" title="${t('download')}">
|
||||
<i class="material-icons">file_download</i>
|
||||
</button>
|
||||
${file.editable ? `
|
||||
<button class="btn btn-sm edit-btn"
|
||||
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||
title="${t('edit')}">
|
||||
<button class="btn btn-sm edit-btn" data-edit-name="${file.name}" data-edit-folder="${file.folder || 'root'}" title="${t('edit')}">
|
||||
<i class="material-icons">edit</i>
|
||||
</button>
|
||||
` : ""}
|
||||
${previewButton}
|
||||
<button class="btn btn-sm btn-warning rename-btn"
|
||||
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||
title="${t('rename')}">
|
||||
<button class="btn btn-sm btn-warning rename-btn" data-rename-name="${file.name}" data-rename-folder="${file.folder || 'root'}" title="${t('rename')}">
|
||||
<i class="material-icons">drive_file_rename_outline</i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -207,10 +217,10 @@ export function buildBottomControls(itemsPerPageSetting) {
|
||||
return `
|
||||
<div class="d-flex align-items-center mt-3 bottom-controls">
|
||||
<label class="label-inline mr-2 mb-0">${t("show")}</label>
|
||||
<select class="form-control bottom-select" onchange="changeItemsPerPage(this.value)">
|
||||
<select class="form-control bottom-select" id="itemsPerPageSelect">
|
||||
${[10, 20, 50, 100]
|
||||
.map(num => `<option value="${num}" ${num === itemsPerPageSetting ? "selected" : ""}>${num}</option>`)
|
||||
.join("")}
|
||||
.map(num => `<option value="${num}" ${num === itemsPerPageSetting ? "selected" : ""}>${num}</option>`)
|
||||
.join("")}
|
||||
</select>
|
||||
<span class="items-per-page-text ml-2 mb-0">${t("items_per_page")}</span>
|
||||
</div>
|
||||
@@ -277,8 +287,6 @@ export function toggleRowSelection(event, fileName) {
|
||||
const start = Math.min(currentIndex, lastIndex);
|
||||
const end = Math.max(currentIndex, lastIndex);
|
||||
|
||||
// If neither CTRL nor Meta is pressed, you might choose
|
||||
// to clear existing selections. For this example we leave existing selections intact.
|
||||
for (let i = start; i <= end; i++) {
|
||||
const cb = allRows[i].querySelector(".file-checkbox");
|
||||
if (cb) {
|
||||
@@ -345,4 +353,7 @@ export function showCustomConfirmModal(message) {
|
||||
yesBtn.addEventListener("click", onYes);
|
||||
noBtn.addEventListener("click", onNo);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.toggleRowSelection = toggleRowSelection;
|
||||
window.updateRowHighlight = updateRowHighlight;
|
||||
@@ -80,16 +80,16 @@ export function openDownloadModal(fileName, folder) {
|
||||
// Store file details globally for the download confirmation function.
|
||||
window.singleFileToDownload = fileName;
|
||||
window.currentFolder = folder || "root";
|
||||
|
||||
|
||||
// Optionally pre-fill the file name input in the modal.
|
||||
const input = document.getElementById("downloadFileNameInput");
|
||||
if (input) {
|
||||
input.value = fileName; // Use file name as-is (or modify if desired)
|
||||
}
|
||||
|
||||
|
||||
// Show the single file download modal (a new modal element).
|
||||
document.getElementById("downloadFileModal").style.display = "block";
|
||||
|
||||
|
||||
// Optionally focus the input after a short delay.
|
||||
setTimeout(() => {
|
||||
if (input) input.focus();
|
||||
@@ -97,58 +97,34 @@ export function openDownloadModal(fileName, folder) {
|
||||
}
|
||||
|
||||
export function confirmSingleDownload() {
|
||||
// Get the file name from the modal. Users can change it if desired.
|
||||
let fileName = document.getElementById("downloadFileNameInput").value.trim();
|
||||
// 1) Get and validate the filename
|
||||
const input = document.getElementById("downloadFileNameInput");
|
||||
const fileName = input.value.trim();
|
||||
if (!fileName) {
|
||||
showToast("Please enter a name for the file.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide the download modal.
|
||||
|
||||
// 2) Hide the download-name modal
|
||||
document.getElementById("downloadFileModal").style.display = "none";
|
||||
// Show the progress modal (same as in your ZIP download flow).
|
||||
document.getElementById("downloadProgressModal").style.display = "block";
|
||||
|
||||
// Build the URL for download.php using GET parameters.
|
||||
|
||||
// 3) Build the direct download URL
|
||||
const folder = window.currentFolder || "root";
|
||||
const downloadURL = "/api/file/download.php?folder=" + encodeURIComponent(folder) +
|
||||
"&file=" + encodeURIComponent(window.singleFileToDownload);
|
||||
|
||||
fetch(downloadURL, {
|
||||
method: "GET",
|
||||
credentials: "include"
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error("Failed to download file: " + text);
|
||||
});
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
if (!blob || blob.size === 0) {
|
||||
throw new Error("Received empty file.");
|
||||
}
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
// Hide the progress modal.
|
||||
document.getElementById("downloadProgressModal").style.display = "none";
|
||||
showToast("Download started.");
|
||||
})
|
||||
.catch(error => {
|
||||
// Hide progress modal and show error.
|
||||
document.getElementById("downloadProgressModal").style.display = "none";
|
||||
console.error("Error downloading file:", error);
|
||||
showToast("Error downloading file: " + error.message);
|
||||
});
|
||||
const downloadURL = "/api/file/download.php"
|
||||
+ "?folder=" + encodeURIComponent(folder)
|
||||
+ "&file=" + encodeURIComponent(window.singleFileToDownload);
|
||||
|
||||
// 4) Trigger native browser download
|
||||
const a = document.createElement("a");
|
||||
a.href = downloadURL;
|
||||
a.download = fileName;
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
// 5) Notify the user
|
||||
showToast("Download started. Check your browser’s download manager.");
|
||||
}
|
||||
|
||||
export function handleExtractZipSelected(e) {
|
||||
@@ -168,16 +144,21 @@ export function handleExtractZipSelected(e) {
|
||||
showToast("No zip files selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Change progress modal text to "Extracting files..."
|
||||
const progressText = document.querySelector("#downloadProgressModal p");
|
||||
if (progressText) {
|
||||
progressText.textContent = "Extracting files...";
|
||||
}
|
||||
|
||||
// Show the progress modal.
|
||||
document.getElementById("downloadProgressModal").style.display = "block";
|
||||
|
||||
|
||||
// Prepare and show the spinner-only modal
|
||||
const modal = document.getElementById("downloadProgressModal");
|
||||
const titleEl = document.getElementById("downloadProgressTitle");
|
||||
const spinner = modal.querySelector(".download-spinner");
|
||||
const progressBar = document.getElementById("downloadProgressBar");
|
||||
const progressPct = document.getElementById("downloadProgressPercent");
|
||||
|
||||
if (titleEl) titleEl.textContent = "Extracting files…";
|
||||
if (spinner) spinner.style.display = "inline-block";
|
||||
if (progressBar) progressBar.style.display = "none";
|
||||
if (progressPct) progressPct.style.display = "none";
|
||||
|
||||
modal.style.display = "block";
|
||||
|
||||
fetch("/api/file/extractZip.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
@@ -192,45 +173,42 @@ export function handleExtractZipSelected(e) {
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Hide the progress modal once the request has completed.
|
||||
document.getElementById("downloadProgressModal").style.display = "none";
|
||||
modal.style.display = "none";
|
||||
if (data.success) {
|
||||
let toastMessage = "Zip file(s) extracted successfully!";
|
||||
if (data.extractedFiles && Array.isArray(data.extractedFiles) && data.extractedFiles.length) {
|
||||
toastMessage = "Extracted: " + data.extractedFiles.join(", ");
|
||||
let msg = "Zip file(s) extracted successfully!";
|
||||
if (Array.isArray(data.extractedFiles) && data.extractedFiles.length) {
|
||||
msg = "Extracted: " + data.extractedFiles.join(", ");
|
||||
}
|
||||
showToast(toastMessage);
|
||||
showToast(msg);
|
||||
loadFileList(window.currentFolder);
|
||||
} else {
|
||||
showToast("Error extracting zip: " + (data.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Hide the progress modal on error.
|
||||
document.getElementById("downloadProgressModal").style.display = "none";
|
||||
modal.style.display = "none";
|
||||
console.error("Error extracting zip files:", error);
|
||||
showToast("Error extracting zip files.");
|
||||
});
|
||||
}
|
||||
|
||||
const extractZipBtn = document.getElementById("extractZipBtn");
|
||||
if (extractZipBtn) {
|
||||
extractZipBtn.replaceWith(extractZipBtn.cloneNode(true));
|
||||
document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected);
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const zipNameModal = document.getElementById("downloadZipModal");
|
||||
const progressModal = document.getElementById("downloadProgressModal");
|
||||
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
||||
const confirmZipBtn = document.getElementById("confirmDownloadZip");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const cancelDownloadZip = document.getElementById("cancelDownloadZip");
|
||||
if (cancelDownloadZip) {
|
||||
cancelDownloadZip.addEventListener("click", function () {
|
||||
document.getElementById("downloadZipModal").style.display = "none";
|
||||
// 1) Cancel button hides the name modal
|
||||
if (cancelZipBtn) {
|
||||
cancelZipBtn.addEventListener("click", () => {
|
||||
zipNameModal.style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
// This part remains in your confirmDownloadZip event handler:
|
||||
const confirmDownloadZip = document.getElementById("confirmDownloadZip");
|
||||
if (confirmDownloadZip) {
|
||||
confirmDownloadZip.addEventListener("click", function () {
|
||||
// 2) Confirm button kicks off the zip+download
|
||||
if (confirmZipBtn) {
|
||||
confirmZipBtn.addEventListener("click", async () => {
|
||||
// a) Validate ZIP filename
|
||||
let zipName = document.getElementById("zipFileNameInput").value.trim();
|
||||
if (!zipName) {
|
||||
showToast("Please enter a name for the zip file.");
|
||||
@@ -239,52 +217,56 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
if (!zipName.toLowerCase().endsWith(".zip")) {
|
||||
zipName += ".zip";
|
||||
}
|
||||
// Hide the ZIP name input modal
|
||||
document.getElementById("downloadZipModal").style.display = "none";
|
||||
// Show the progress modal here only on confirm
|
||||
console.log("Download confirmed. Showing progress modal.");
|
||||
document.getElementById("downloadProgressModal").style.display = "block";
|
||||
const folder = window.currentFolder || "root";
|
||||
fetch("/api/file/downloadZip.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ folder: folder, files: window.filesToDownload })
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error("Failed to create zip file: " + text);
|
||||
});
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
if (!blob || blob.size === 0) {
|
||||
throw new Error("Received empty zip file.");
|
||||
}
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
a.href = url;
|
||||
a.download = zipName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
// Hide the progress modal after download starts
|
||||
document.getElementById("downloadProgressModal").style.display = "none";
|
||||
showToast("Download started.");
|
||||
})
|
||||
.catch(error => {
|
||||
// Hide the progress modal on error
|
||||
document.getElementById("downloadProgressModal").style.display = "none";
|
||||
console.error("Error downloading zip:", error);
|
||||
showToast("Error downloading selected files as zip: " + error.message);
|
||||
|
||||
// b) Hide the name‐input modal, show the spinner modal
|
||||
zipNameModal.style.display = "none";
|
||||
progressModal.style.display = "block";
|
||||
|
||||
// c) (Optional) update the “Preparing…” text if you gave it an ID
|
||||
const titleEl = document.getElementById("downloadProgressTitle");
|
||||
if (titleEl) titleEl.textContent = `Preparing ${zipName}…`;
|
||||
|
||||
try {
|
||||
// d) POST and await the ZIP blob
|
||||
const res = await fetch("/api/file/downloadZip.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder: window.currentFolder || "root",
|
||||
files: window.filesToDownload
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
throw new Error(txt || `Status ${res.status}`);
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
if (!blob || blob.size === 0) {
|
||||
throw new Error("Received empty ZIP file.");
|
||||
}
|
||||
|
||||
// e) Hand off to the browser’s download manager
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = zipName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error downloading ZIP:", err);
|
||||
showToast("Error: " + err.message);
|
||||
} finally {
|
||||
// f) Always hide spinner modal
|
||||
progressModal.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -573,4 +555,22 @@ export function initFileActions() {
|
||||
}
|
||||
}
|
||||
|
||||
// Hook up the single‐file download modal buttons
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const cancelDownloadFileBtn = document.getElementById("cancelDownloadFile");
|
||||
if (cancelDownloadFileBtn) {
|
||||
cancelDownloadFileBtn.addEventListener("click", () => {
|
||||
document.getElementById("downloadFileModal").style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
const confirmSingleDownloadBtn = document.getElementById("confirmSingleDownloadButton");
|
||||
if (confirmSingleDownloadBtn) {
|
||||
confirmSingleDownloadBtn.addEventListener("click", confirmSingleDownload);
|
||||
}
|
||||
|
||||
// Make Enter also confirm the download
|
||||
attachEnterKeyListener("downloadFileModal", "confirmSingleDownloadButton");
|
||||
});
|
||||
|
||||
window.renameFile = renameFile;
|
||||
@@ -340,6 +340,88 @@ export function renderFileTable(folder, container) {
|
||||
|
||||
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||
|
||||
// pagination clicks
|
||||
const prevBtn = document.getElementById("prevPageBtn");
|
||||
if (prevBtn) prevBtn.addEventListener("click", () => {
|
||||
if (window.currentPage > 1) {
|
||||
window.currentPage--;
|
||||
renderFileTable(folder, container);
|
||||
}
|
||||
});
|
||||
const nextBtn = document.getElementById("nextPageBtn");
|
||||
if (nextBtn) nextBtn.addEventListener("click", () => {
|
||||
// totalPages is computed above in this scope
|
||||
if (window.currentPage < totalPages) {
|
||||
window.currentPage++;
|
||||
renderFileTable(folder, container);
|
||||
}
|
||||
});
|
||||
|
||||
// ADD: advanced search toggle
|
||||
const advToggle = document.getElementById("advancedSearchToggle");
|
||||
if (advToggle) advToggle.addEventListener("click", () => {
|
||||
toggleAdvancedSearch();
|
||||
});
|
||||
|
||||
// items-per-page selector
|
||||
const itemsSelect = document.getElementById("itemsPerPageSelect");
|
||||
if (itemsSelect) itemsSelect.addEventListener("change", e => {
|
||||
window.itemsPerPage = parseInt(e.target.value, 10);
|
||||
localStorage.setItem("itemsPerPage", window.itemsPerPage);
|
||||
window.currentPage = 1;
|
||||
renderFileTable(folder, container);
|
||||
});
|
||||
|
||||
// hook up the master checkbox
|
||||
const selectAll = document.getElementById("selectAll");
|
||||
if (selectAll) {
|
||||
selectAll.addEventListener("change", () => {
|
||||
toggleAllCheckboxes(selectAll);
|
||||
});
|
||||
}
|
||||
|
||||
// 1) Row-click selects the row
|
||||
fileListContent.querySelectorAll("tbody tr").forEach(row => {
|
||||
row.addEventListener("click", e => {
|
||||
// grab the underlying checkbox value
|
||||
const cb = row.querySelector(".file-checkbox");
|
||||
if (!cb) return;
|
||||
toggleRowSelection(e, cb.value);
|
||||
});
|
||||
});
|
||||
|
||||
// 2) Download buttons
|
||||
fileListContent.querySelectorAll(".download-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
openDownloadModal(btn.dataset.downloadName, btn.dataset.downloadFolder);
|
||||
});
|
||||
});
|
||||
|
||||
// 3) Edit buttons
|
||||
fileListContent.querySelectorAll(".edit-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
editFile(btn.dataset.editName, btn.dataset.editFolder);
|
||||
});
|
||||
});
|
||||
|
||||
// 4) Rename buttons
|
||||
fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
renameFile(btn.dataset.renameName, btn.dataset.renameFolder);
|
||||
});
|
||||
});
|
||||
|
||||
// 5) Preview buttons (if you still have a .preview-btn)
|
||||
fileListContent.querySelectorAll(".preview-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
previewFile(btn.dataset.previewUrl, btn.dataset.previewName);
|
||||
});
|
||||
});
|
||||
|
||||
createViewToggleButton();
|
||||
|
||||
// Setup event listeners.
|
||||
@@ -476,23 +558,26 @@ export function renderGalleryView(folder, container) {
|
||||
|
||||
pageFiles.forEach((file, idx) => {
|
||||
const idSafe = encodeURIComponent(file.name) + "-" + (startIdx + idx);
|
||||
const cacheKey = folderPath + encodeURIComponent(file.name);
|
||||
|
||||
// thumbnail
|
||||
let thumbnail;
|
||||
if (/\.(jpe?g|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
||||
const cacheKey = folderPath + encodeURIComponent(file.name);
|
||||
if (window.imageCache && window.imageCache[cacheKey]) {
|
||||
thumbnail = `<img src="${window.imageCache[cacheKey]}"
|
||||
class="gallery-thumbnail"
|
||||
alt="${escapeHTML(file.name)}"
|
||||
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||||
thumbnail = `<img
|
||||
src="${window.imageCache[cacheKey]}"
|
||||
class="gallery-thumbnail"
|
||||
data-cache-key="${cacheKey}"
|
||||
alt="${escapeHTML(file.name)}"
|
||||
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||||
} else {
|
||||
const imageUrl = folderPath + encodeURIComponent(file.name) + "?t=" + Date.now();
|
||||
thumbnail = `<img src="${imageUrl}"
|
||||
onload="cacheImage(this,'${cacheKey}')"
|
||||
class="gallery-thumbnail"
|
||||
alt="${escapeHTML(file.name)}"
|
||||
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||||
thumbnail = `<img
|
||||
src="${imageUrl}"
|
||||
class="gallery-thumbnail"
|
||||
data-cache-key="${cacheKey}"
|
||||
alt="${escapeHTML(file.name)}"
|
||||
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||||
}
|
||||
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
|
||||
@@ -529,9 +614,9 @@ export function renderGalleryView(folder, container) {
|
||||
<label for="cb-${idSafe}"
|
||||
style="position:absolute; top:5px; left:5px; width:16px; height:16px;"></label>
|
||||
|
||||
<div class="gallery-preview"
|
||||
style="cursor:pointer;"
|
||||
onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t='+Date.now(), '${file.name}')">
|
||||
<div class="gallery-preview" style="cursor:pointer;"
|
||||
data-preview-url="${folderPath + encodeURIComponent(file.name)}?t=${Date.now()}"
|
||||
data-preview-name="${file.name}">
|
||||
${thumbnail}
|
||||
</div>
|
||||
|
||||
@@ -544,22 +629,25 @@ export function renderGalleryView(folder, container) {
|
||||
|
||||
<div class="button-wrap" style="display:flex; justify-content:center; gap:5px; margin-top:5px;">
|
||||
<button type="button" class="btn btn-sm btn-success download-btn"
|
||||
onclick="openDownloadModal('${file.name}', '${file.folder || "root"}')"
|
||||
data-download-name="${escapeHTML(file.name)}"
|
||||
data-download-folder="${file.folder || "root"}"
|
||||
title="${t('download')}">
|
||||
<i class="material-icons">file_download</i>
|
||||
</button>
|
||||
${file.editable ? `
|
||||
<button class="btn btn-sm edit-btn"
|
||||
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||
title="${t('Edit')}">
|
||||
<button type="button" class="btn btn-sm edit-btn"
|
||||
data-edit-name="${escapeHTML(file.name)}"
|
||||
data-edit-folder="${file.folder || "root"}"
|
||||
title="${t('edit')}">
|
||||
<i class="material-icons">edit</i>
|
||||
</button>` : ""}
|
||||
<button class="btn btn-sm btn-warning rename-btn"
|
||||
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||
<button type="button" class="btn btn-sm btn-warning rename-btn"
|
||||
data-rename-name="${escapeHTML(file.name)}"
|
||||
data-rename-folder="${file.folder || "root"}"
|
||||
title="${t('rename')}">
|
||||
<i class="material-icons">drive_file_rename_outline</i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary share-btn"
|
||||
<button type="button" class="btn btn-sm btn-secondary share-btn"
|
||||
data-file="${escapeHTML(file.name)}"
|
||||
title="${t('share')}">
|
||||
<i class="material-icons">share</i>
|
||||
@@ -579,13 +667,93 @@ export function renderGalleryView(folder, container) {
|
||||
// render
|
||||
fileListContent.innerHTML = galleryHTML;
|
||||
|
||||
// ensure toggle button
|
||||
createViewToggleButton();
|
||||
// --- Now wire up all behaviors without inline handlers ---
|
||||
|
||||
// attach listeners
|
||||
// ADD: pagination buttons for gallery
|
||||
const prevBtn = document.getElementById("prevPageBtn");
|
||||
if (prevBtn) prevBtn.addEventListener("click", () => {
|
||||
if (window.currentPage > 1) {
|
||||
window.currentPage--;
|
||||
renderGalleryView(folder, container);
|
||||
}
|
||||
});
|
||||
const nextBtn = document.getElementById("nextPageBtn");
|
||||
if (nextBtn) nextBtn.addEventListener("click", () => {
|
||||
if (window.currentPage < totalPages) {
|
||||
window.currentPage++;
|
||||
renderGalleryView(folder, container);
|
||||
}
|
||||
});
|
||||
|
||||
// ←— ADD: advanced search toggle
|
||||
const advToggle = document.getElementById("advancedSearchToggle");
|
||||
if (advToggle) advToggle.addEventListener("click", () => {
|
||||
toggleAdvancedSearch();
|
||||
});
|
||||
|
||||
// ←— ADD: wire up context-menu in gallery
|
||||
bindFileListContextMenu();
|
||||
|
||||
// ADD: items-per-page selector for gallery
|
||||
const itemsSelect = document.getElementById("itemsPerPageSelect");
|
||||
if (itemsSelect) itemsSelect.addEventListener("change", e => {
|
||||
window.itemsPerPage = parseInt(e.target.value, 10);
|
||||
localStorage.setItem("itemsPerPage", window.itemsPerPage);
|
||||
window.currentPage = 1;
|
||||
renderGalleryView(folder, container);
|
||||
});
|
||||
|
||||
// cache images on load
|
||||
fileListContent.querySelectorAll('.gallery-thumbnail').forEach(img => {
|
||||
const key = img.dataset.cacheKey;
|
||||
img.addEventListener('load', () => cacheImage(img, key));
|
||||
});
|
||||
|
||||
// preview clicks
|
||||
fileListContent.querySelectorAll(".gallery-preview").forEach(el => {
|
||||
el.addEventListener("click", () => {
|
||||
previewFile(el.dataset.previewUrl, el.dataset.previewName);
|
||||
});
|
||||
});
|
||||
|
||||
// download clicks
|
||||
fileListContent.querySelectorAll(".download-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
openDownloadModal(btn.dataset.downloadName, btn.dataset.downloadFolder);
|
||||
});
|
||||
});
|
||||
|
||||
// edit clicks
|
||||
fileListContent.querySelectorAll(".edit-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
editFile(btn.dataset.editName, btn.dataset.editFolder);
|
||||
});
|
||||
});
|
||||
|
||||
// rename clicks
|
||||
fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
renameFile(btn.dataset.renameName, btn.dataset.renameFolder);
|
||||
});
|
||||
});
|
||||
|
||||
// share clicks
|
||||
fileListContent.querySelectorAll(".share-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
const fileName = btn.dataset.file;
|
||||
const fileObj = fileData.find(f => f.name === fileName);
|
||||
if (fileObj) {
|
||||
import('./filePreview.js').then(m => m.openShareModal(fileObj, folder));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// checkboxes
|
||||
document.querySelectorAll(".file-checkbox").forEach(cb => {
|
||||
fileListContent.querySelectorAll(".file-checkbox").forEach(cb => {
|
||||
cb.addEventListener("change", () => updateFileActionButtons());
|
||||
});
|
||||
|
||||
@@ -603,14 +771,13 @@ export function renderGalleryView(folder, container) {
|
||||
});
|
||||
}
|
||||
|
||||
// pagination
|
||||
// pagination functions
|
||||
window.changePage = newPage => {
|
||||
window.currentPage = newPage;
|
||||
if (window.viewMode === "gallery") renderGalleryView(folder);
|
||||
else renderFileTable(folder);
|
||||
};
|
||||
|
||||
// items per page
|
||||
window.changeItemsPerPage = cnt => {
|
||||
window.itemsPerPage = +cnt;
|
||||
localStorage.setItem("itemsPerPage", cnt);
|
||||
@@ -619,8 +786,9 @@ export function renderGalleryView(folder, container) {
|
||||
else renderFileTable(folder);
|
||||
};
|
||||
|
||||
// update toolbar buttons
|
||||
// update toolbar and toggle button
|
||||
updateFileActionButtons();
|
||||
createViewToggleButton();
|
||||
}
|
||||
|
||||
// Responsive slider constraints based on screen size.
|
||||
|
||||
@@ -4,36 +4,68 @@ import { fileData } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
export function openShareModal(file, folder) {
|
||||
// Remove any existing modal
|
||||
const existing = document.getElementById("shareModal");
|
||||
if (existing) existing.remove();
|
||||
|
||||
// Build the modal
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "shareModal";
|
||||
modal.classList.add("modal");
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content share-modal-content" style="width: 600px; max-width:90vw;">
|
||||
<div class="modal-content share-modal-content" style="width:600px;max-width:90vw;">
|
||||
<div class="modal-header">
|
||||
<h3>${t("share_file")}: ${escapeHTML(file.name)}</h3>
|
||||
<span class="close-image-modal" id="closeShareModal" title="Close">×</span>
|
||||
<span id="closeShareModal" title="${t("close")}" class="close-image-modal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>${t("set_expiration")}</p>
|
||||
<select id="shareExpiration">
|
||||
<option value="30">30 minutes</option>
|
||||
<option value="60" selected>60 minutes</option>
|
||||
<option value="120">120 minutes</option>
|
||||
<option value="180">180 minutes</option>
|
||||
<option value="240">240 minutes</option>
|
||||
<option value="1440">1 Day</option>
|
||||
<select id="shareExpiration" style="width:100%;padding:5px;">
|
||||
<option value="30">30 ${t("minutes")}</option>
|
||||
<option value="60" selected>60 ${t("minutes")}</option>
|
||||
<option value="120">120 ${t("minutes")}</option>
|
||||
<option value="180">180 ${t("minutes")}</option>
|
||||
<option value="240">240 ${t("minutes")}</option>
|
||||
<option value="1440">1 ${t("day")}</option>
|
||||
<option value="custom">${t("custom")}…</option>
|
||||
</select>
|
||||
<p>${t("password_optional")}</p>
|
||||
<input type="text" id="sharePassword" placeholder=${t("password_optional")} style="width: 100%;"/>
|
||||
<br>
|
||||
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">${t("generate_share_link")}</button>
|
||||
<div id="shareLinkDisplay" style="margin-top: 10px; display:none;">
|
||||
|
||||
<div id="customExpirationContainer" style="display:none;margin-top:10px;">
|
||||
<label for="customExpirationValue">${t("duration")}:</label>
|
||||
<input type="number" id="customExpirationValue" min="1" value="1" style="width:60px;margin:0 8px;"/>
|
||||
<select id="customExpirationUnit">
|
||||
<option value="seconds">${t("seconds")}</option>
|
||||
<option value="minutes" selected>${t("minutes")}</option>
|
||||
<option value="hours">${t("hours")}</option>
|
||||
<option value="days">${t("days")}</option>
|
||||
</select>
|
||||
<p class="share-warning" style="color:#a33;font-size:0.9em;margin-top:5px;">
|
||||
${t("custom_duration_warning")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="margin-top:15px;">${t("password_optional")}</p>
|
||||
<input
|
||||
type="text"
|
||||
id="sharePassword"
|
||||
placeholder="${t("password_optional")}"
|
||||
style="width:100%;padding:5px;"
|
||||
/>
|
||||
|
||||
<button
|
||||
id="generateShareLinkBtn"
|
||||
class="btn btn-primary"
|
||||
style="margin-top:15px;"
|
||||
>
|
||||
${t("generate_share_link")}
|
||||
</button>
|
||||
|
||||
<div id="shareLinkDisplay" style="margin-top:15px;display:none;">
|
||||
<p>${t("shareable_link")}</p>
|
||||
<input type="text" id="shareLinkInput" readonly style="width:100%;"/>
|
||||
<button id="copyShareLinkBtn" class="btn btn-primary" style="margin-top:5px;">${t("copy_link")}</button>
|
||||
<input type="text" id="shareLinkInput" readonly style="width:100%;padding:5px;"/>
|
||||
<button id="copyShareLinkBtn" class="btn btn-secondary" style="margin-top:5px;">
|
||||
${t("copy_link")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,52 +73,72 @@ export function openShareModal(file, folder) {
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = "block";
|
||||
|
||||
document.getElementById("closeShareModal").addEventListener("click", () => {
|
||||
modal.remove();
|
||||
});
|
||||
// Close handler
|
||||
document.getElementById("closeShareModal")
|
||||
.addEventListener("click", () => modal.remove());
|
||||
|
||||
document.getElementById("generateShareLinkBtn").addEventListener("click", () => {
|
||||
const expiration = document.getElementById("shareExpiration").value;
|
||||
const password = document.getElementById("sharePassword").value;
|
||||
fetch("/api/file/createShareLink.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder: folder,
|
||||
file: file.name,
|
||||
expirationMinutes: parseInt(expiration),
|
||||
password: password
|
||||
// Show/hide custom-duration inputs
|
||||
document.getElementById("shareExpiration")
|
||||
.addEventListener("change", e => {
|
||||
const container = document.getElementById("customExpirationContainer");
|
||||
container.style.display = e.target.value === "custom" ? "block" : "none";
|
||||
});
|
||||
|
||||
// Generate share link
|
||||
document.getElementById("generateShareLinkBtn")
|
||||
.addEventListener("click", () => {
|
||||
const sel = document.getElementById("shareExpiration");
|
||||
let value, unit;
|
||||
|
||||
if (sel.value === "custom") {
|
||||
value = parseInt(document.getElementById("customExpirationValue").value, 10);
|
||||
unit = document.getElementById("customExpirationUnit").value;
|
||||
} else {
|
||||
value = parseInt(sel.value, 10);
|
||||
unit = "minutes";
|
||||
}
|
||||
|
||||
const password = document.getElementById("sharePassword").value;
|
||||
|
||||
fetch("/api/file/createShareLink.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder,
|
||||
file: file.name,
|
||||
expirationValue: value,
|
||||
expirationUnit: unit,
|
||||
password
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.token) {
|
||||
const shareEndpoint = `${window.location.origin}/api/file/share.php`;
|
||||
const shareUrl = `${shareEndpoint}?token=${encodeURIComponent(data.token)}`;
|
||||
const displayDiv = document.getElementById("shareLinkDisplay");
|
||||
const inputField = document.getElementById("shareLinkInput");
|
||||
inputField.value = shareUrl;
|
||||
displayDiv.style.display = "block";
|
||||
const url = `${window.location.origin}/api/file/share.php?token=${encodeURIComponent(data.token)}`;
|
||||
document.getElementById("shareLinkInput").value = url;
|
||||
document.getElementById("shareLinkDisplay").style.display = "block";
|
||||
} else {
|
||||
showToast("Error generating share link: " + (data.error || "Unknown error"));
|
||||
showToast(t("error_generating_share") + ": " + (data.error||"Unknown"));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error generating share link:", err);
|
||||
showToast("Error generating share link.");
|
||||
console.error(err);
|
||||
showToast(t("error_generating_share"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("copyShareLinkBtn").addEventListener("click", () => {
|
||||
const input = document.getElementById("shareLinkInput");
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
showToast("Link copied to clipboard!");
|
||||
});
|
||||
// Copy to clipboard
|
||||
document.getElementById("copyShareLinkBtn")
|
||||
.addEventListener("click", () => {
|
||||
const input = document.getElementById("shareLinkInput");
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
showToast(t("link_copied"));
|
||||
});
|
||||
}
|
||||
|
||||
export function previewFile(fileUrl, fileName) {
|
||||
@@ -364,16 +416,21 @@ export function previewFile(fileUrl, fileName) {
|
||||
}
|
||||
} else {
|
||||
// Handle non-image file previews.
|
||||
if (extension === "pdf") {
|
||||
const embed = document.createElement("embed");
|
||||
const separator = fileUrl.indexOf('?') === -1 ? '?' : '&';
|
||||
embed.src = fileUrl + separator + 't=' + new Date().getTime();
|
||||
embed.type = "application/pdf";
|
||||
embed.style.width = "80vw";
|
||||
embed.style.height = "80vh";
|
||||
embed.style.border = "none";
|
||||
container.appendChild(embed);
|
||||
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(fileName)) {
|
||||
if (extension === "pdf") {
|
||||
// build a cache‐busted URL
|
||||
const separator = fileUrl.includes('?') ? '&' : '?';
|
||||
const urlWithTs = fileUrl + separator + 't=' + Date.now();
|
||||
|
||||
// open in a new tab (avoids CSP frame-ancestors)
|
||||
window.open(urlWithTs, "_blank");
|
||||
|
||||
// tear down the just-created modal
|
||||
const modal = document.getElementById("filePreviewModal");
|
||||
if (modal) modal.remove();
|
||||
|
||||
// stop further preview logic
|
||||
return;
|
||||
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(fileName)) {
|
||||
const video = document.createElement("video");
|
||||
video.src = fileUrl;
|
||||
video.controls = true;
|
||||
|
||||
@@ -13,10 +13,19 @@ export function openTagModal(file) {
|
||||
modal.id = 'tagModal';
|
||||
modal.className = 'modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="width: 400px; max-width:90vw;">
|
||||
<div class="modal-content" style="width: 450px; max-width:90vw;">
|
||||
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0;">${t("tag_file")}: ${file.name}</h3>
|
||||
<span id="closeTagModal" style="cursor:pointer; font-size:24px;">×</span>
|
||||
<h3 style="
|
||||
margin:0;
|
||||
display:inline-block;
|
||||
max-width: calc(100% - 40px);
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
">
|
||||
${t("tag_file")}: ${escapeHTML(file.name)}
|
||||
</h3>
|
||||
<span id="closeTagModal" class="editor-close-btn">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="margin-top:10px;">
|
||||
<label for="tagNameInput">${t("tag_name")}</label>
|
||||
@@ -83,10 +92,10 @@ export function openMultiTagModal(files) {
|
||||
modal.id = 'multiTagModal';
|
||||
modal.className = 'modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="width: 400px; max-width:90vw;">
|
||||
<div class="modal-content" style="width: 450px; max-width:90vw;">
|
||||
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0;">Tag Selected Files (${files.length})</h3>
|
||||
<span id="closeMultiTagModal" style="cursor:pointer; font-size:24px;">×</span>
|
||||
<span id="closeMultiTagModal" class="editor-close-btn">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="margin-top:10px;">
|
||||
<label for="multiTagNameInput">Tag Name:</label>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { loadFileList } from './fileListView.js';
|
||||
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
import { openFolderShareModal } from './folderShareModal.js';
|
||||
import { fetchWithCsrf } from './auth.js';
|
||||
import { loadCsrfToken } from './main.js';
|
||||
|
||||
/* ----------------------
|
||||
Helper Functions (Data/State)
|
||||
@@ -102,24 +104,26 @@ export function setupBreadcrumbDelegation() {
|
||||
|
||||
// Click handler via delegation
|
||||
function breadcrumbClickHandler(e) {
|
||||
// find the nearest .breadcrumb-link
|
||||
const link = e.target.closest(".breadcrumb-link");
|
||||
if (!link) return;
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const folder = link.getAttribute("data-folder");
|
||||
const folder = link.dataset.folder;
|
||||
window.currentFolder = folder;
|
||||
localStorage.setItem("lastOpenedFolder", folder);
|
||||
|
||||
// Update the container with sanitized breadcrumbs.
|
||||
const container = document.getElementById("fileListTitle");
|
||||
const sanitizedBreadcrumb = DOMPurify.sanitize(renderBreadcrumb(folder));
|
||||
container.innerHTML = t("files_in") + " (" + sanitizedBreadcrumb + ")";
|
||||
|
||||
// rebuild the title safely
|
||||
updateBreadcrumbTitle(folder);
|
||||
expandTreePath(folder);
|
||||
document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||
const targetOption = document.querySelector(`.folder-option[data-folder="${folder}"]`);
|
||||
if (targetOption) targetOption.classList.add("selected");
|
||||
document.querySelectorAll(".folder-option").forEach(el =>
|
||||
el.classList.remove("selected")
|
||||
);
|
||||
const target = document.querySelector(`.folder-option[data-folder="${folder}"]`);
|
||||
if (target) target.classList.add("selected");
|
||||
|
||||
loadFileList(folder);
|
||||
}
|
||||
|
||||
@@ -333,11 +337,43 @@ function folderDropHandler(event) {
|
||||
/* ----------------------
|
||||
Main Folder Tree Rendering and Event Binding
|
||||
----------------------*/
|
||||
// --- Helpers for safe breadcrumb rendering ---
|
||||
function renderBreadcrumbFragment(folderPath) {
|
||||
const frag = document.createDocumentFragment();
|
||||
const parts = folderPath.split("/");
|
||||
let acc = "";
|
||||
|
||||
parts.forEach((part, idx) => {
|
||||
acc = idx === 0 ? part : acc + "/" + part;
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.classList.add("breadcrumb-link");
|
||||
span.dataset.folder = acc;
|
||||
span.textContent = part;
|
||||
frag.appendChild(span);
|
||||
|
||||
if (idx < parts.length - 1) {
|
||||
frag.appendChild(document.createTextNode(" / "));
|
||||
}
|
||||
});
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
function updateBreadcrumbTitle(folder) {
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
titleEl.textContent = "";
|
||||
titleEl.appendChild(document.createTextNode(t("files_in") + " ("));
|
||||
titleEl.appendChild(renderBreadcrumbFragment(folder));
|
||||
titleEl.appendChild(document.createTextNode(")"));
|
||||
setupBreadcrumbDelegation();
|
||||
}
|
||||
|
||||
export async function loadFolderTree(selectedFolder) {
|
||||
try {
|
||||
// Check if the user has folder-only permission.
|
||||
await checkUserFolderPermission();
|
||||
|
||||
|
||||
// Determine effective root folder.
|
||||
const username = localStorage.getItem("username") || "root";
|
||||
let effectiveRoot = "root";
|
||||
@@ -351,14 +387,14 @@ export async function loadFolderTree(selectedFolder) {
|
||||
} else {
|
||||
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
||||
}
|
||||
|
||||
|
||||
// Build fetch URL.
|
||||
let fetchUrl = '/api/folder/getFolderList.php';
|
||||
if (window.userFolderOnly) {
|
||||
fetchUrl += '?restricted=1';
|
||||
}
|
||||
console.log("Fetching folder list from:", fetchUrl);
|
||||
|
||||
|
||||
// Fetch folder list from the server.
|
||||
const response = await fetch(fetchUrl);
|
||||
if (response.status === 401) {
|
||||
@@ -375,10 +411,10 @@ export async function loadFolderTree(selectedFolder) {
|
||||
} else if (Array.isArray(folderData)) {
|
||||
folders = folderData;
|
||||
}
|
||||
|
||||
|
||||
// Remove any global "root" entry.
|
||||
folders = folders.filter(folder => folder.toLowerCase() !== "root");
|
||||
|
||||
|
||||
// If restricted, filter folders: keep only those that start with effectiveRoot + "/" (do not include effectiveRoot itself).
|
||||
if (window.userFolderOnly && effectiveRoot !== "root") {
|
||||
folders = folders.filter(folder => folder.startsWith(effectiveRoot + "/"));
|
||||
@@ -386,16 +422,16 @@ export async function loadFolderTree(selectedFolder) {
|
||||
localStorage.setItem("lastOpenedFolder", effectiveRoot);
|
||||
window.currentFolder = effectiveRoot;
|
||||
}
|
||||
|
||||
|
||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||
|
||||
|
||||
// Render the folder tree.
|
||||
const container = document.getElementById("folderTreeContainer");
|
||||
if (!container) {
|
||||
console.error("Folder tree container not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let html = `<div id="rootRow" class="root-row">
|
||||
<span class="folder-toggle" data-folder="${effectiveRoot}">[<span class="custom-dash">-</span>]</span>
|
||||
<span class="folder-option root-folder-option" data-folder="${effectiveRoot}">${effectiveLabel}</span>
|
||||
@@ -405,35 +441,35 @@ export async function loadFolderTree(selectedFolder) {
|
||||
html += renderFolderTree(tree, "", "block");
|
||||
}
|
||||
container.innerHTML = html;
|
||||
|
||||
|
||||
// Attach drag/drop event listeners.
|
||||
container.querySelectorAll(".folder-option").forEach(el => {
|
||||
el.addEventListener("dragover", folderDragOverHandler);
|
||||
el.addEventListener("dragleave", folderDragLeaveHandler);
|
||||
el.addEventListener("drop", folderDropHandler);
|
||||
});
|
||||
|
||||
|
||||
if (selectedFolder) {
|
||||
window.currentFolder = selectedFolder;
|
||||
}
|
||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(window.currentFolder) + ")";
|
||||
setupBreadcrumbDelegation();
|
||||
|
||||
// Initial breadcrumb update
|
||||
updateBreadcrumbTitle(window.currentFolder);
|
||||
loadFileList(window.currentFolder);
|
||||
|
||||
|
||||
const folderState = loadFolderTreeState();
|
||||
if (window.currentFolder !== effectiveRoot && folderState[window.currentFolder] !== "none") {
|
||||
expandTreePath(window.currentFolder);
|
||||
}
|
||||
|
||||
|
||||
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
||||
if (selectedEl) {
|
||||
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||
selectedEl.classList.add("selected");
|
||||
}
|
||||
|
||||
|
||||
// Folder-option click: update selection, breadcrumbs, and file list
|
||||
container.querySelectorAll(".folder-option").forEach(el => {
|
||||
el.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
@@ -442,13 +478,14 @@ export async function loadFolderTree(selectedFolder) {
|
||||
const selected = this.getAttribute("data-folder");
|
||||
window.currentFolder = selected;
|
||||
localStorage.setItem("lastOpenedFolder", selected);
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(selected) + ")";
|
||||
setupBreadcrumbDelegation();
|
||||
|
||||
// Safe breadcrumb update
|
||||
updateBreadcrumbTitle(selected);
|
||||
loadFileList(selected);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Root toggle handler
|
||||
const rootToggle = container.querySelector("#rootRow .folder-toggle");
|
||||
if (rootToggle) {
|
||||
rootToggle.addEventListener("click", function (e) {
|
||||
@@ -471,7 +508,8 @@ export async function loadFolderTree(selectedFolder) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Other folder-toggle handlers
|
||||
container.querySelectorAll(".folder-toggle").forEach(toggle => {
|
||||
toggle.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
@@ -494,12 +532,13 @@ export async function loadFolderTree(selectedFolder) {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading folder tree:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// For backward compatibility.
|
||||
export function loadFolderList(selectedFolder) {
|
||||
loadFolderTree(selectedFolder);
|
||||
@@ -627,45 +666,53 @@ document.getElementById("cancelCreateFolder").addEventListener("click", function
|
||||
document.getElementById("newFolderName").value = "";
|
||||
});
|
||||
attachEnterKeyListener("createFolderModal", "submitCreateFolder");
|
||||
document.getElementById("submitCreateFolder").addEventListener("click", function () {
|
||||
document.getElementById("submitCreateFolder").addEventListener("click", async () => {
|
||||
const folderInput = document.getElementById("newFolderName").value.trim();
|
||||
if (!folderInput) {
|
||||
showToast("Please enter a folder name.");
|
||||
return;
|
||||
if (!folderInput) return showToast("Please enter a folder name.");
|
||||
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
const parent = selectedFolder === "root" ? "" : selectedFolder;
|
||||
|
||||
// 1) Guarantee fresh CSRF
|
||||
try {
|
||||
await loadCsrfToken();
|
||||
} catch {
|
||||
return showToast("Could not refresh CSRF token. Please reload.");
|
||||
}
|
||||
let selectedFolder = window.currentFolder || "root";
|
||||
let fullFolderName = folderInput;
|
||||
if (selectedFolder && selectedFolder !== "root") {
|
||||
fullFolderName = selectedFolder + "/" + folderInput;
|
||||
}
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
fetch("/api/folder/createFolder.php", {
|
||||
|
||||
// 2) Call with fetchWithCsrf
|
||||
fetchWithCsrf("/api/folder/createFolder.php", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folderName: folderInput,
|
||||
parent: selectedFolder === "root" ? "" : selectedFolder
|
||||
})
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folderName: folderInput, parent })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("Folder created successfully!");
|
||||
window.currentFolder = fullFolderName;
|
||||
localStorage.setItem("lastOpenedFolder", fullFolderName);
|
||||
loadFolderList(fullFolderName);
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not create folder"));
|
||||
.then(async res => {
|
||||
if (!res.ok) {
|
||||
// pull out a JSON error, or fallback to status text
|
||||
let err;
|
||||
try {
|
||||
const j = await res.json();
|
||||
err = j.error || j.message || res.statusText;
|
||||
} catch {
|
||||
err = res.statusText;
|
||||
}
|
||||
throw new Error(err);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
showToast("Folder created!");
|
||||
const full = parent ? `${parent}/${folderInput}` : folderInput;
|
||||
window.currentFolder = full;
|
||||
localStorage.setItem("lastOpenedFolder", full);
|
||||
loadFolderList(full);
|
||||
})
|
||||
.catch(e => {
|
||||
showToast("Error creating folder: " + e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
document.getElementById("createFolderModal").style.display = "none";
|
||||
document.getElementById("newFolderName").value = "";
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error creating folder:", error);
|
||||
document.getElementById("createFolderModal").style.display = "none";
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,44 +1,75 @@
|
||||
// folderShareModal.js
|
||||
// js/folderShareModal.js
|
||||
import { escapeHTML, showToast } from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
export function openFolderShareModal(folder) {
|
||||
// Remove any existing folder share modal
|
||||
// Remove any existing modal
|
||||
const existing = document.getElementById("folderShareModal");
|
||||
if (existing) existing.remove();
|
||||
|
||||
// Create the modal container
|
||||
// Build modal
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "folderShareModal";
|
||||
modal.classList.add("modal");
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content share-modal-content" style="width: 600px; max-width: 90vw;">
|
||||
<div class="modal-content share-modal-content" style="width:600px;max-width:90vw;">
|
||||
<div class="modal-header">
|
||||
<h3>${t("share_folder")}: ${escapeHTML(folder)}</h3>
|
||||
<span class="close-image-modal" id="closeFolderShareModal" title="Close">×</span>
|
||||
<span id="closeFolderShareModal" title="${t("close")}" class="close-image-modal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>${t("set_expiration")}</p>
|
||||
<select id="folderShareExpiration">
|
||||
<select id="folderShareExpiration" style="width:100%;padding:5px;">
|
||||
<option value="30">30 ${t("minutes")}</option>
|
||||
<option value="60" selected>60 ${t("minutes")}</option>
|
||||
<option value="120">120 ${t("minutes")}</option>
|
||||
<option value="180">180 ${t("minutes")}</option>
|
||||
<option value="240">240 ${t("minutes")}</option>
|
||||
<option value="1440">1 ${t("day")}</option>
|
||||
<option value="custom">${t("custom")}…</option>
|
||||
</select>
|
||||
<p>${t("password_optional")}</p>
|
||||
<input type="text" id="folderSharePassword" placeholder="${t("enter_password")}" style="width: 100%;"/>
|
||||
<br>
|
||||
<label>
|
||||
<input type="checkbox" id="folderShareAllowUpload"> ${t("allow_uploads")}
|
||||
|
||||
<div id="customFolderExpirationContainer" style="display:none;margin-top:10px;">
|
||||
<label for="customFolderExpirationValue">${t("duration")}:</label>
|
||||
<input type="number" id="customFolderExpirationValue" min="1" value="1" style="width:60px;margin:0 8px;"/>
|
||||
<select id="customFolderExpirationUnit">
|
||||
<option value="seconds">${t("seconds")}</option>
|
||||
<option value="minutes" selected>${t("minutes")}</option>
|
||||
<option value="hours">${t("hours")}</option>
|
||||
<option value="days">${t("days")}</option>
|
||||
</select>
|
||||
<p class="share-warning" style="color:#a33;font-size:0.9em;margin-top:5px;">
|
||||
${t("custom_duration_warning")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="margin-top:15px;">${t("password_optional")}</p>
|
||||
<input
|
||||
type="text"
|
||||
id="folderSharePassword"
|
||||
placeholder="${t("enter_password")}"
|
||||
style="width:100%;padding:5px;"
|
||||
/>
|
||||
|
||||
<label style="margin-top:10px;display:block;">
|
||||
<input type="checkbox" id="folderShareAllowUpload" />
|
||||
${t("allow_uploads")}
|
||||
</label>
|
||||
<br><br>
|
||||
<button id="generateFolderShareLinkBtn" class="btn btn-primary" style="margin-top: 10px;">${t("generate_share_link")}</button>
|
||||
<div id="folderShareLinkDisplay" style="margin-top: 10px; display: none;">
|
||||
|
||||
<button
|
||||
id="generateFolderShareLinkBtn"
|
||||
class="btn btn-primary"
|
||||
style="margin-top:15px;"
|
||||
>
|
||||
${t("generate_share_link")}
|
||||
</button>
|
||||
|
||||
<div id="folderShareLinkDisplay" style="margin-top:15px;display:none;">
|
||||
<p>${t("shareable_link")}</p>
|
||||
<input type="text" id="folderShareLinkInput" readonly style="width: 100%;"/>
|
||||
<button id="copyFolderShareLinkBtn" class="btn btn-primary" style="margin-top: 5px;">${t("copy_link")}</button>
|
||||
<input type="text" id="folderShareLinkInput" readonly style="width:100%;padding:5px;"/>
|
||||
<button id="copyFolderShareLinkBtn" class="btn btn-secondary" style="margin-top:5px;">
|
||||
${t("copy_link")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,62 +77,75 @@ export function openFolderShareModal(folder) {
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = "block";
|
||||
|
||||
// Close button handler
|
||||
document.getElementById("closeFolderShareModal").addEventListener("click", () => {
|
||||
modal.remove();
|
||||
});
|
||||
// Close
|
||||
document.getElementById("closeFolderShareModal")
|
||||
.addEventListener("click", () => modal.remove());
|
||||
|
||||
// Handler for generating the share link
|
||||
document.getElementById("generateFolderShareLinkBtn").addEventListener("click", () => {
|
||||
const expiration = document.getElementById("folderShareExpiration").value;
|
||||
const password = document.getElementById("folderSharePassword").value;
|
||||
const allowUpload = document.getElementById("folderShareAllowUpload").checked ? 1 : 0;
|
||||
|
||||
// Retrieve the CSRF token from the meta tag.
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
|
||||
if (!csrfToken) {
|
||||
showToast(t("csrf_error"));
|
||||
return;
|
||||
}
|
||||
// Post to the createFolderShareLink endpoint.
|
||||
fetch("/api/folder/createShareFolderLink.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder: folder,
|
||||
expirationMinutes: parseInt(expiration, 10),
|
||||
password: password,
|
||||
allowUpload: allowUpload
|
||||
// Toggle custom inputs
|
||||
document.getElementById("folderShareExpiration")
|
||||
.addEventListener("change", e => {
|
||||
document.getElementById("customFolderExpirationContainer")
|
||||
.style.display = e.target.value === "custom" ? "block" : "none";
|
||||
});
|
||||
|
||||
// Generate link
|
||||
document.getElementById("generateFolderShareLinkBtn")
|
||||
.addEventListener("click", () => {
|
||||
const sel = document.getElementById("folderShareExpiration");
|
||||
let value, unit;
|
||||
if (sel.value === "custom") {
|
||||
value = parseInt(document.getElementById("customFolderExpirationValue").value, 10);
|
||||
unit = document.getElementById("customFolderExpirationUnit").value;
|
||||
} else {
|
||||
value = parseInt(sel.value, 10);
|
||||
unit = "minutes";
|
||||
}
|
||||
|
||||
const password = document.getElementById("folderSharePassword").value;
|
||||
const allowUpload = document.getElementById("folderShareAllowUpload").checked ? 1 : 0;
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
|
||||
if (!csrfToken) {
|
||||
showToast(t("csrf_error"));
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/api/folder/createShareFolderLink.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder,
|
||||
expirationValue: value,
|
||||
expirationUnit: unit,
|
||||
password,
|
||||
allowUpload
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.token && data.link) {
|
||||
const shareUrl = data.link;
|
||||
const displayDiv = document.getElementById("folderShareLinkDisplay");
|
||||
const inputField = document.getElementById("folderShareLinkInput");
|
||||
inputField.value = shareUrl;
|
||||
displayDiv.style.display = "block";
|
||||
document.getElementById("folderShareLinkInput").value = data.link;
|
||||
document.getElementById("folderShareLinkDisplay").style.display = "block";
|
||||
showToast(t("share_link_generated"));
|
||||
} else {
|
||||
showToast(t("error_generating_share_link") + ": " + (data.error || t("unknown_error")));
|
||||
showToast(t("error_generating_share_link") + ": " + (data.error||t("unknown_error")));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error generating folder share link:", err);
|
||||
showToast(t("error_generating_share_link") + ": " + (err.error || t("unknown_error")));
|
||||
console.error(err);
|
||||
showToast(t("error_generating_share_link") + ": " + t("unknown_error"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Copy share link button handler
|
||||
document.getElementById("copyFolderShareLinkBtn").addEventListener("click", () => {
|
||||
const input = document.getElementById("folderShareLinkInput");
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
showToast(t("link_copied"));
|
||||
});
|
||||
// Copy
|
||||
document.getElementById("copyFolderShareLinkBtn")
|
||||
.addEventListener("click", () => {
|
||||
const inp = document.getElementById("folderShareLinkInput");
|
||||
inp.select();
|
||||
document.execCommand("copy");
|
||||
showToast(t("link_copied"));
|
||||
});
|
||||
}
|
||||
@@ -55,6 +55,7 @@ const translations = {
|
||||
// Additional keys for HTML translations:
|
||||
"title": "FileRise",
|
||||
"header_title": "FileRise",
|
||||
"header_title_text": "Header Title",
|
||||
"logout": "Logout",
|
||||
"change_password": "Change Password",
|
||||
"restore_text": "Restore or",
|
||||
@@ -150,6 +151,13 @@ const translations = {
|
||||
"allow_uploads": "Allow Uploads",
|
||||
"share_link_generated": "Share Link Generated",
|
||||
"error_generating_share_link": "Error Generating Share Link",
|
||||
"custom": "Custom",
|
||||
"duration": "Duration",
|
||||
"seconds": "Seconds",
|
||||
"minutes": "Minutes",
|
||||
"hours": "Hours",
|
||||
"days": "Days",
|
||||
"custom_duration_warning": "⚠️ Using a long expiration may pose security risks. Use with caution.",
|
||||
|
||||
// Folder
|
||||
"folder_share": "Share Folder",
|
||||
@@ -166,16 +174,30 @@ const translations = {
|
||||
"user": "User:",
|
||||
"unknown_error": "Unknown Error",
|
||||
"link_copied": "Link Copied to Clipboard",
|
||||
"minutes": "minutes",
|
||||
"hours": "hours",
|
||||
"days": "days",
|
||||
"weeks": "weeks",
|
||||
"months": "months",
|
||||
"seconds": "seconds",
|
||||
|
||||
// Dark Mode Toggle
|
||||
"dark_mode_toggle": "Dark Mode",
|
||||
"light_mode_toggle": "Light Mode",
|
||||
"switch_to_light_mode": "Switch to light mode",
|
||||
"switch_to_dark_mode": "Switch to dark mode",
|
||||
|
||||
// Admin Panel
|
||||
"header_settings": "Header Settings",
|
||||
"shared_max_upload_size_bytes_title": "Shared Max Upload Size",
|
||||
"shared_max_upload_size_bytes": "Shared Max Upload Size (bytes)",
|
||||
"max_bytes_shared_uploads_note": "Enter maximum bytes allowed for shared-folder uploads",
|
||||
"manage_shared_links": "Manage Shared Links",
|
||||
"folder_shares": "Folder Shares",
|
||||
"file_shares": "File Shares",
|
||||
"loading": "Loading…",
|
||||
"error_loading_share_links": "Error loading share links",
|
||||
"share_deleted_successfully": "Share deleted successfully",
|
||||
"error_deleting_share": "Error deleting share",
|
||||
"password_protected": "Password protected",
|
||||
"no_shared_links_available": "No shared links available",
|
||||
|
||||
|
||||
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
|
||||
"admin_panel": "Admin Panel",
|
||||
@@ -237,7 +259,7 @@ const translations = {
|
||||
"ok": "OK",
|
||||
"show": "Show",
|
||||
"items_per_page": "items per page",
|
||||
"columns":"Columns",
|
||||
"columns": "Columns",
|
||||
"api_docs": "API Docs"
|
||||
},
|
||||
es: {
|
||||
@@ -295,6 +317,7 @@ const translations = {
|
||||
// Additional keys for HTML translations:
|
||||
"title": "FileRise",
|
||||
"header_title": "FileRise",
|
||||
"header_title_text": "Header Title",
|
||||
"logout": "Cerrar sesión",
|
||||
"change_password": "Cambiar contraseña",
|
||||
"restore_text": "Restaurar o",
|
||||
@@ -804,7 +827,7 @@ const translations = {
|
||||
"prev": "Zurück",
|
||||
"next": "Weiter",
|
||||
"page": "Seite",
|
||||
"of": "von",
|
||||
"of": "von",
|
||||
|
||||
// Login Form keys:
|
||||
"login": "Anmelden",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import { initUpload } from './upload.js';
|
||||
import { initAuth, checkAuthentication, loadAdminConfigFunc } from './auth.js';
|
||||
import { initAuth, fetchWithCsrf, checkAuthentication, loadAdminConfigFunc } from './auth.js';
|
||||
const _originalFetch = window.fetch;
|
||||
window.fetch = fetchWithCsrf;
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
||||
@@ -12,39 +14,83 @@ import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload }
|
||||
import { editFile, saveFile } from './fileEditor.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
|
||||
// Remove the retry logic version and just use loadCsrfToken directly:
|
||||
function loadCsrfToken() {
|
||||
return fetch('/api/auth/token.php', { credentials: 'include' })
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Token fetch failed with status: " + response.status);
|
||||
export function initializeApp() {
|
||||
window.currentFolder = "root";
|
||||
initTagSearch();
|
||||
loadFileList(window.currentFolder);
|
||||
initDragAndDrop();
|
||||
loadSidebarOrder();
|
||||
loadHeaderOrder();
|
||||
initFileActions();
|
||||
initUpload();
|
||||
loadFolderTree();
|
||||
setupTrashRestoreDelete();
|
||||
loadAdminConfigFunc();
|
||||
|
||||
const helpBtn = document.getElementById("folderHelpBtn");
|
||||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||||
if (helpBtn && helpTooltip) {
|
||||
helpBtn.addEventListener("click", () => {
|
||||
helpTooltip.style.display =
|
||||
helpTooltip.style.display === "block" ? "none" : "block";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function loadCsrfToken() {
|
||||
return fetchWithCsrf('/api/auth/token.php', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`Token fetch failed with status ${res.status}`);
|
||||
}
|
||||
return response.json();
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
window.csrfToken = data.csrf_token;
|
||||
window.SHARE_URL = data.share_url;
|
||||
|
||||
let metaCSRF = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!metaCSRF) {
|
||||
metaCSRF = document.createElement('meta');
|
||||
metaCSRF.name = 'csrf-token';
|
||||
document.head.appendChild(metaCSRF);
|
||||
.then(({ csrf_token, share_url }) => {
|
||||
// Update global and <meta>
|
||||
window.csrfToken = csrf_token;
|
||||
let meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.name = 'csrf-token';
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
metaCSRF.setAttribute('content', data.csrf_token);
|
||||
meta.content = csrf_token;
|
||||
|
||||
let metaShare = document.querySelector('meta[name="share-url"]');
|
||||
if (!metaShare) {
|
||||
metaShare = document.createElement('meta');
|
||||
metaShare.name = 'share-url';
|
||||
document.head.appendChild(metaShare);
|
||||
let shareMeta = document.querySelector('meta[name="share-url"]');
|
||||
if (!shareMeta) {
|
||||
shareMeta = document.createElement('meta');
|
||||
shareMeta.name = 'share-url';
|
||||
document.head.appendChild(shareMeta);
|
||||
}
|
||||
metaShare.setAttribute('content', data.share_url);
|
||||
shareMeta.content = share_url;
|
||||
|
||||
return data;
|
||||
return { csrf_token, share_url };
|
||||
});
|
||||
}
|
||||
|
||||
// 1) Immediately clear “?logout=1” flag
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('logout') === '1') {
|
||||
localStorage.removeItem("username");
|
||||
localStorage.removeItem("userTOTPEnabled");
|
||||
}
|
||||
|
||||
// 2) Wire up logoutBtn right away
|
||||
const logoutBtn = document.getElementById("logoutBtn");
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener("click", () => {
|
||||
fetch("/api/auth/logout.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
})
|
||||
.then(() => window.location.reload(true))
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Expose functions for inline handlers.
|
||||
window.sendRequest = sendRequest;
|
||||
@@ -76,31 +122,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
// Continue with initializations that rely on a valid CSRF token:
|
||||
checkAuthentication().then(authenticated => {
|
||||
if (authenticated) {
|
||||
window.currentFolder = "root";
|
||||
initTagSearch();
|
||||
loadFileList(window.currentFolder);
|
||||
initDragAndDrop();
|
||||
loadSidebarOrder();
|
||||
loadHeaderOrder();
|
||||
initFileActions();
|
||||
initUpload();
|
||||
loadFolderTree();
|
||||
setupTrashRestoreDelete();
|
||||
loadAdminConfigFunc();
|
||||
|
||||
const helpBtn = document.getElementById("folderHelpBtn");
|
||||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||||
helpBtn.addEventListener("click", function () {
|
||||
// Toggle display of the tooltip.
|
||||
if (helpTooltip.style.display === "none" || helpTooltip.style.display === "") {
|
||||
helpTooltip.style.display = "block";
|
||||
} else {
|
||||
helpTooltip.style.display = "none";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn("User not authenticated. Data loading deferred.");
|
||||
}
|
||||
document.getElementById('loadingOverlay').remove();
|
||||
initializeApp();
|
||||
}
|
||||
});
|
||||
|
||||
// Other DOM initialization that can happen after CSRF is ready.
|
||||
@@ -115,48 +139,55 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
// --- Dark Mode Persistence ---
|
||||
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||
const storedDarkMode = localStorage.getItem("darkMode");
|
||||
const darkModeIcon = document.getElementById("darkModeIcon");
|
||||
|
||||
if (storedDarkMode === "true") {
|
||||
document.body.classList.add("dark-mode");
|
||||
} else if (storedDarkMode === "false") {
|
||||
document.body.classList.remove("dark-mode");
|
||||
} else {
|
||||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
document.body.classList.add("dark-mode");
|
||||
} else {
|
||||
document.body.classList.remove("dark-mode");
|
||||
if (darkModeToggle && darkModeIcon) {
|
||||
// 1) Load stored preference (or null)
|
||||
let stored = localStorage.getItem("darkMode");
|
||||
const hasStored = stored !== null;
|
||||
|
||||
// 2) Determine initial mode
|
||||
const isDark = hasStored
|
||||
? (stored === "true")
|
||||
: (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
|
||||
document.body.classList.toggle("dark-mode", isDark);
|
||||
darkModeToggle.classList.toggle("active", isDark);
|
||||
|
||||
// 3) Helper to update icon & aria-label
|
||||
function updateIcon() {
|
||||
const dark = document.body.classList.contains("dark-mode");
|
||||
darkModeIcon.textContent = dark ? "light_mode" : "dark_mode";
|
||||
darkModeToggle.setAttribute(
|
||||
"aria-label",
|
||||
dark ? t("light_mode") : t("dark_mode")
|
||||
);
|
||||
darkModeToggle.setAttribute(
|
||||
"title",
|
||||
dark
|
||||
? t("switch_to_light_mode")
|
||||
: t("switch_to_dark_mode")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (darkModeToggle) {
|
||||
darkModeToggle.textContent = document.body.classList.contains("dark-mode")
|
||||
? t("light_mode")
|
||||
: t("dark_mode");
|
||||
updateIcon();
|
||||
|
||||
darkModeToggle.addEventListener("click", function () {
|
||||
if (document.body.classList.contains("dark-mode")) {
|
||||
document.body.classList.remove("dark-mode");
|
||||
localStorage.setItem("darkMode", "false");
|
||||
darkModeToggle.textContent = t("dark_mode");
|
||||
} else {
|
||||
document.body.classList.add("dark-mode");
|
||||
localStorage.setItem("darkMode", "true");
|
||||
darkModeToggle.textContent = t("light_mode");
|
||||
}
|
||||
// 4) Click handler: always override and store preference
|
||||
darkModeToggle.addEventListener("click", () => {
|
||||
const nowDark = document.body.classList.toggle("dark-mode");
|
||||
localStorage.setItem("darkMode", nowDark ? "true" : "false");
|
||||
updateIcon();
|
||||
});
|
||||
}
|
||||
|
||||
if (localStorage.getItem("darkMode") === null && window.matchMedia) {
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (event) => {
|
||||
if (event.matches) {
|
||||
document.body.classList.add("dark-mode");
|
||||
if (darkModeToggle) darkModeToggle.textContent = t("light_mode");
|
||||
} else {
|
||||
document.body.classList.remove("dark-mode");
|
||||
if (darkModeToggle) darkModeToggle.textContent = t("dark_mode");
|
||||
}
|
||||
});
|
||||
// 5) OS‐level change: only if no stored pref at load
|
||||
if (!hasStored && window.matchMedia) {
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", e => {
|
||||
document.body.classList.toggle("dark-mode", e.matches);
|
||||
updateIcon();
|
||||
});
|
||||
}
|
||||
}
|
||||
// --- End Dark Mode Persistence ---
|
||||
|
||||
|
||||
6
public/js/redoc-init.js
Normal file
6
public/js/redoc-init.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// public/js/redoc-init.js
|
||||
if (!customElements.get('redoc')) {
|
||||
Redoc.init(window.location.origin + '/api.php?spec=1',
|
||||
{},
|
||||
document.getElementById('redoc-container'));
|
||||
}
|
||||
90
public/js/sharedFolderView.js
Normal file
90
public/js/sharedFolderView.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// sharedFolderView.js
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let viewMode = 'list';
|
||||
const payload = JSON.parse(
|
||||
document.getElementById('shared-data').textContent
|
||||
);
|
||||
const token = payload.token;
|
||||
const filesData = payload.files;
|
||||
const downloadBase = `${window.location.origin}/api/folder/downloadSharedFile.php?token=${encodeURIComponent(token)}&file=`;
|
||||
const btn = document.getElementById('toggleBtn');
|
||||
if (btn) btn.classList.add('toggle-btn');
|
||||
|
||||
function toggleViewMode() {
|
||||
const listEl = document.getElementById('listViewContainer');
|
||||
const galleryEl = document.getElementById('galleryViewContainer');
|
||||
|
||||
if (viewMode === 'list') {
|
||||
viewMode = 'gallery';
|
||||
listEl.style.display = 'none';
|
||||
renderGalleryView();
|
||||
galleryEl.style.display = 'block';
|
||||
btn.textContent = 'Switch to List View';
|
||||
} else {
|
||||
viewMode = 'list';
|
||||
galleryEl.style.display = 'none';
|
||||
listEl.style.display = 'block';
|
||||
btn.textContent = 'Switch to Gallery View';
|
||||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', toggleViewMode);
|
||||
|
||||
function renderGalleryView() {
|
||||
const container = document.getElementById('galleryViewContainer');
|
||||
// clear previous
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'shared-gallery-container';
|
||||
|
||||
filesData.forEach(file => {
|
||||
const url = downloadBase + encodeURIComponent(file);
|
||||
const ext = file.split('.').pop().toLowerCase();
|
||||
const isImg = /^(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/.test(ext);
|
||||
|
||||
// card
|
||||
const card = document.createElement('div');
|
||||
card.className = 'shared-gallery-card';
|
||||
|
||||
// preview
|
||||
const preview = document.createElement('div');
|
||||
preview.className = 'gallery-preview';
|
||||
preview.style.cursor = 'pointer';
|
||||
preview.dataset.url = url;
|
||||
|
||||
if (isImg) {
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.alt = file; // safe, file is not HTML
|
||||
preview.appendChild(img);
|
||||
} else {
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'material-icons';
|
||||
icon.textContent = 'insert_drive_file';
|
||||
preview.appendChild(icon);
|
||||
}
|
||||
card.appendChild(preview);
|
||||
|
||||
// info
|
||||
const info = document.createElement('div');
|
||||
info.className = 'gallery-info';
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'gallery-file-name';
|
||||
nameSpan.textContent = file; // textContent escapes any HTML
|
||||
info.appendChild(nameSpan);
|
||||
card.appendChild(info);
|
||||
|
||||
grid.appendChild(card);
|
||||
|
||||
preview.addEventListener('click', () => {
|
||||
window.location.href = preview.dataset.url;
|
||||
});
|
||||
});
|
||||
|
||||
container.appendChild(grid);
|
||||
}
|
||||
|
||||
window.renderGalleryView = renderGalleryView;
|
||||
});
|
||||
@@ -412,7 +412,12 @@ function initResumableUpload() {
|
||||
forceChunkSize: true,
|
||||
testChunks: false,
|
||||
throttleProgressCallbacks: 1,
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
withCredentials: true,
|
||||
headers: { 'X-CSRF-Token': window.csrfToken },
|
||||
query: {
|
||||
folder: window.currentFolder || "root",
|
||||
upload_token: window.csrfToken // still as a fallback
|
||||
}
|
||||
});
|
||||
|
||||
const fileInput = document.getElementById("file");
|
||||
@@ -496,26 +501,40 @@ function initResumableUpload() {
|
||||
});
|
||||
|
||||
resumableInstance.on("fileSuccess", function(file, message) {
|
||||
const li = document.querySelector(`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`);
|
||||
// Try to parse JSON response
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(message);
|
||||
} catch (e) {
|
||||
data = null;
|
||||
}
|
||||
|
||||
// 1) Soft‐fail CSRF? then update token & retry this file
|
||||
if (data && data.csrf_expired) {
|
||||
// Update global and Resumable headers
|
||||
window.csrfToken = data.csrf_token;
|
||||
resumableInstance.opts.headers['X-CSRF-Token'] = data.csrf_token;
|
||||
resumableInstance.opts.query.upload_token = data.csrf_token;
|
||||
// Retry this chunk/file
|
||||
file.retry();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Otherwise treat as real success:
|
||||
const li = document.querySelector(
|
||||
`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`
|
||||
);
|
||||
if (li && li.progressBar) {
|
||||
li.progressBar.style.width = "100%";
|
||||
li.progressBar.innerText = "Done";
|
||||
// Hide pause/resume and remove buttons for successful files.
|
||||
// remove action buttons
|
||||
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
|
||||
if (pauseResumeBtn) {
|
||||
pauseResumeBtn.style.display = "none";
|
||||
}
|
||||
if (pauseResumeBtn) pauseResumeBtn.style.display = "none";
|
||||
const removeBtn = li.querySelector(".remove-file-btn");
|
||||
if (removeBtn) {
|
||||
removeBtn.style.display = "none";
|
||||
}
|
||||
// Schedule removal of the file entry after 5 seconds.
|
||||
setTimeout(() => {
|
||||
li.remove();
|
||||
window.selectedFiles = window.selectedFiles.filter(f => f.uniqueIdentifier !== file.uniqueIdentifier);
|
||||
updateFileInfoCount();
|
||||
}, 5000);
|
||||
if (removeBtn) removeBtn.style.display = "none";
|
||||
setTimeout(() => li.remove(), 5000);
|
||||
}
|
||||
|
||||
loadFileList(window.currentFolder);
|
||||
});
|
||||
|
||||
@@ -618,8 +637,25 @@ function submitFiles(allFiles) {
|
||||
} catch (e) {
|
||||
jsonResponse = null;
|
||||
}
|
||||
|
||||
// ─── Soft-fail CSRF: retry this upload ───────────────────────
|
||||
if (jsonResponse && jsonResponse.csrf_expired) {
|
||||
console.warn("CSRF expired during upload, retrying chunk", file.uploadIndex);
|
||||
// 1) update global token + header
|
||||
window.csrfToken = jsonResponse.csrf_token;
|
||||
xhr.open("POST", "/api/upload/upload.php", true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
||||
// 2) re-send the same formData
|
||||
xhr.send(formData);
|
||||
return; // skip the "finishedCount++" and error/success logic for now
|
||||
}
|
||||
|
||||
// ─── Normal success/error handling ────────────────────────────
|
||||
const li = progressElements[file.uploadIndex];
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300 && (!jsonResponse || !jsonResponse.error)) {
|
||||
// real success
|
||||
if (li) {
|
||||
li.progressBar.style.width = "100%";
|
||||
li.progressBar.innerText = "Done";
|
||||
@@ -627,11 +663,14 @@ function submitFiles(allFiles) {
|
||||
}
|
||||
uploadResults[file.uploadIndex] = true;
|
||||
} else {
|
||||
// real failure
|
||||
if (li) {
|
||||
li.progressBar.innerText = "Error";
|
||||
}
|
||||
allSucceeded = false;
|
||||
}
|
||||
|
||||
// ─── Only now count this chunk as finished ───────────────────
|
||||
finishedCount++;
|
||||
if (finishedCount === allFiles.length) {
|
||||
refreshFileList(allFiles, uploadResults, progressElements);
|
||||
@@ -665,6 +704,7 @@ function submitFiles(allFiles) {
|
||||
});
|
||||
|
||||
xhr.open("POST", "/api/upload/upload.php", true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
||||
xhr.send(formData);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
// public/webdav.php
|
||||
|
||||
// ─── 0) Forward Basic auth into PHP_AUTH_* for every HTTP verb ─────────────
|
||||
if (
|
||||
empty($_SERVER['PHP_AUTH_USER'])
|
||||
&& !empty($_SERVER['HTTP_AUTHORIZATION'])
|
||||
@@ -11,46 +12,58 @@ if (
|
||||
$_SERVER['PHP_AUTH_PW'] = $p;
|
||||
}
|
||||
|
||||
// ─── 1) Bootstrap & load models ─────────────────────────────────────────────
|
||||
require_once __DIR__ . '/../config/config.php'; // UPLOAD_DIR, META_DIR, DATE_TIME_FORMAT
|
||||
require_once __DIR__ . '/../vendor/autoload.php'; // Composer & SabreDAV
|
||||
require_once __DIR__ . '/../src/models/AuthModel.php'; // AuthModel::authenticate(), getUserRole(), loadFolderPermission()
|
||||
require_once __DIR__ . '/../src/models/AdminModel.php'; // AdminModel::getConfig()
|
||||
|
||||
// ─── 3) Load your WebDAV directory implementation ──────────────────────────
|
||||
// ─── 1.1) Global WebDAV feature toggle ──────────────────────────────────────
|
||||
$adminConfig = AdminModel::getConfig();
|
||||
$enableWebDAV = isset($adminConfig['enableWebDAV']) && $adminConfig['enableWebDAV'];
|
||||
if (!$enableWebDAV) {
|
||||
header('HTTP/1.1 403 Forbidden');
|
||||
echo 'WebDAV access is currently disabled by administrator.';
|
||||
exit;
|
||||
}
|
||||
|
||||
// ─── 2) Load WebDAV directory implementation ──────────────────────────
|
||||
require_once __DIR__ . '/../src/webdav/FileRiseDirectory.php';
|
||||
use Sabre\DAV\Server;
|
||||
use Sabre\DAV\Auth\Backend\BasicCallBack;
|
||||
use Sabre\DAV\Auth\Plugin as AuthPlugin;
|
||||
use Sabre\DAV\Browser\Plugin as BrowserPlugin;
|
||||
use Sabre\DAV\Locks\Plugin as LocksPlugin;
|
||||
use Sabre\DAV\Locks\Backend\File as LocksFileBackend;
|
||||
use FileRise\WebDAV\FileRiseDirectory;
|
||||
|
||||
// ─── 3) HTTP‑Basic backend ─────────────────────────────────────────────────
|
||||
$authBackend = new BasicCallBack(function(string $user, string $pass) {
|
||||
return \AuthModel::authenticate($user, $pass) !== false;
|
||||
});
|
||||
$authPlugin = new AuthPlugin($authBackend, 'FileRise');
|
||||
|
||||
// ─── 4) Determine user scope ────────────────────────────────────────────────
|
||||
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
|
||||
$isAdmin = (\AuthModel::getUserRole($user) === '1');
|
||||
$folderOnly = (bool)\AuthModel::loadFolderPermission($user);
|
||||
|
||||
if ($isAdmin || !$folderOnly) {
|
||||
// admins or unrestricted users see the full /uploads
|
||||
// Admins (or users without folder-only restriction) see the full /uploads
|
||||
$rootPath = rtrim(UPLOAD_DIR, '/\\');
|
||||
} else {
|
||||
// folder‑only users see only /uploads/{username}
|
||||
// Folder‑only users see only /uploads/{username}
|
||||
$rootPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $user;
|
||||
if (!is_dir($rootPath)) {
|
||||
mkdir($rootPath, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 5) Spin up SabreDAV ────────────────────────────────────────────────────
|
||||
$server = new Server([
|
||||
new FileRiseDirectory($rootPath, $user, $folderOnly),
|
||||
]);
|
||||
|
||||
$server->addPlugin($authPlugin);
|
||||
//$server->addPlugin(new BrowserPlugin()); // optional HTML browser UI
|
||||
$server->addPlugin(
|
||||
new LocksPlugin(
|
||||
new LocksFileBackend(sys_get_temp_dir() . '/sabre-locksdb')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
// src/controllers/adminController.php
|
||||
// src/controllers/AdminController.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||||
@@ -35,7 +35,9 @@ class AdminController
|
||||
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
||||
* @OA\Property(property="disableOIDCLogin", type="boolean", example=false)
|
||||
* ),
|
||||
* @OA\Property(property="globalOtpauthUrl", type="string", example="")
|
||||
* @OA\Property(property="globalOtpauthUrl", type="string", example=""),
|
||||
* @OA\Property(property="enableWebDAV", type="boolean", example=false),
|
||||
* @OA\Property(property="sharedMaxUploadSize", type="integer", example=52428800)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
@@ -88,7 +90,9 @@ class AdminController
|
||||
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
||||
* @OA\Property(property="disableOIDCLogin", type="boolean", example=false)
|
||||
* ),
|
||||
* @OA\Property(property="globalOtpauthUrl", type="string", example="")
|
||||
* @OA\Property(property="globalOtpauthUrl", type="string", example=""),
|
||||
* @OA\Property(property="enableWebDAV", type="boolean", example=false),
|
||||
* @OA\Property(property="sharedMaxUploadSize", type="integer", example=52428800)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
@@ -149,7 +153,7 @@ class AdminController
|
||||
exit;
|
||||
}
|
||||
|
||||
// Prepare configuration array.
|
||||
// Prepare existing settings
|
||||
$headerTitle = isset($data['header_title']) ? trim($data['header_title']) : "";
|
||||
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
|
||||
$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : '';
|
||||
@@ -183,20 +187,38 @@ class AdminController
|
||||
}
|
||||
$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : "";
|
||||
|
||||
// ── NEW: enableWebDAV flag ──────────────────────────────────────
|
||||
$enableWebDAV = false;
|
||||
if (array_key_exists('enableWebDAV', $data)) {
|
||||
$enableWebDAV = filter_var($data['enableWebDAV'], FILTER_VALIDATE_BOOLEAN);
|
||||
} elseif (isset($data['features']['enableWebDAV'])) {
|
||||
$enableWebDAV = filter_var($data['features']['enableWebDAV'], FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
// ── NEW: sharedMaxUploadSize ──────────────────────────────────────
|
||||
$sharedMaxUploadSize = null;
|
||||
if (array_key_exists('sharedMaxUploadSize', $data)) {
|
||||
$sharedMaxUploadSize = filter_var($data['sharedMaxUploadSize'], FILTER_VALIDATE_INT);
|
||||
} elseif (isset($data['features']['sharedMaxUploadSize'])) {
|
||||
$sharedMaxUploadSize = filter_var($data['features']['sharedMaxUploadSize'], FILTER_VALIDATE_INT);
|
||||
}
|
||||
|
||||
$configUpdate = [
|
||||
'header_title' => $headerTitle,
|
||||
'oidc' => [
|
||||
'providerUrl' => $oidcProviderUrl,
|
||||
'clientId' => $oidcClientId,
|
||||
'clientSecret' => $oidcClientSecret,
|
||||
'redirectUri' => $oidcRedirectUri,
|
||||
'header_title' => $headerTitle,
|
||||
'oidc' => [
|
||||
'providerUrl' => $oidcProviderUrl,
|
||||
'clientId' => $oidcClientId,
|
||||
'clientSecret' => $oidcClientSecret,
|
||||
'redirectUri' => $oidcRedirectUri,
|
||||
],
|
||||
'loginOptions' => [
|
||||
'loginOptions' => [
|
||||
'disableFormLogin' => $disableFormLogin,
|
||||
'disableBasicAuth' => $disableBasicAuth,
|
||||
'disableOIDCLogin' => $disableOIDCLogin,
|
||||
],
|
||||
'globalOtpauthUrl' => $globalOtpauthUrl
|
||||
'globalOtpauthUrl' => $globalOtpauthUrl,
|
||||
'enableWebDAV' => $enableWebDAV,
|
||||
'sharedMaxUploadSize' => $sharedMaxUploadSize // ← NEW
|
||||
];
|
||||
|
||||
// Delegate to the model.
|
||||
@@ -207,4 +229,4 @@ class AdminController
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
// src/controllers/authController.php
|
||||
// src/controllers/AuthController.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/AuthModel.php';
|
||||
@@ -238,28 +238,28 @@ class AuthController
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiry = time() + 30 * 24 * 60 * 60;
|
||||
$all = [];
|
||||
|
||||
|
||||
if (file_exists($tokFile)) {
|
||||
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||
$all = json_decode($dec, true) ?: [];
|
||||
}
|
||||
|
||||
|
||||
$all[$token] = [
|
||||
'username' => $username,
|
||||
'expiry' => $expiry,
|
||||
'isAdmin' => $_SESSION['isAdmin']
|
||||
];
|
||||
|
||||
|
||||
file_put_contents(
|
||||
$tokFile,
|
||||
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||
LOCK_EX
|
||||
);
|
||||
|
||||
|
||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
|
||||
|
||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||
|
||||
|
||||
setcookie(
|
||||
session_name(),
|
||||
session_id(),
|
||||
@@ -269,7 +269,7 @@ class AuthController
|
||||
$secure,
|
||||
true
|
||||
);
|
||||
|
||||
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
|
||||
@@ -341,40 +341,86 @@ class AuthController
|
||||
|
||||
public function checkAuth(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// 1) Remember-me re-login
|
||||
if (empty($_SESSION['authenticated']) && !empty($_COOKIE['remember_me_token'])) {
|
||||
$payload = AuthModel::validateRememberToken($_COOKIE['remember_me_token']);
|
||||
if ($payload) {
|
||||
$old = $_SESSION['csrf_token'] ?? bin2hex(random_bytes(32));
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['csrf_token'] = $old;
|
||||
$_SESSION['authenticated'] = true;
|
||||
$_SESSION['username'] = $payload['username'];
|
||||
$_SESSION['isAdmin'] = !empty($payload['isAdmin']);
|
||||
$_SESSION['folderOnly'] = $payload['folderOnly'] ?? false;
|
||||
$_SESSION['readOnly'] = $payload['readOnly'] ?? false;
|
||||
$_SESSION['disableUpload'] = $payload['disableUpload'] ?? false;
|
||||
// regenerate CSRF if you use one
|
||||
|
||||
|
||||
// TOTP enabled? (same logic as below)
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
$totp = false;
|
||||
if (file_exists($usersFile)) {
|
||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
$parts = explode(':', trim($line));
|
||||
if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) {
|
||||
$totp = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'authenticated' => true,
|
||||
'csrf_token' => $_SESSION['csrf_token'],
|
||||
'isAdmin' => $_SESSION['isAdmin'],
|
||||
'totp_enabled' => $totp,
|
||||
'username' => $_SESSION['username'],
|
||||
'folderOnly' => $_SESSION['folderOnly'],
|
||||
'readOnly' => $_SESSION['readOnly'],
|
||||
'disableUpload' => $_SESSION['disableUpload']
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
|
||||
// setup mode?
|
||||
// 2) Setup mode?
|
||||
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
|
||||
error_log("checkAuth: setup mode");
|
||||
echo json_encode(['setup' => true]);
|
||||
exit();
|
||||
}
|
||||
|
||||
// 3) Session-based auth
|
||||
if (empty($_SESSION['authenticated'])) {
|
||||
echo json_encode(['authenticated' => false]);
|
||||
exit();
|
||||
}
|
||||
|
||||
// TOTP enabled?
|
||||
// 4) TOTP enabled?
|
||||
$totp = false;
|
||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
$parts = explode(':', trim($line));
|
||||
if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) {
|
||||
if ($parts[0] === ($_SESSION['username'] ?? '') && !empty($parts[3])) {
|
||||
$totp = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$isAdmin = ((int)AuthModel::getUserRole($_SESSION['username']) === 1);
|
||||
// 5) Final response
|
||||
$resp = [
|
||||
'authenticated' => true,
|
||||
'isAdmin' => $isAdmin,
|
||||
'totp_enabled' => $totp,
|
||||
'username' => $_SESSION['username'],
|
||||
'folderOnly' => $_SESSION['folderOnly'] ?? false,
|
||||
'readOnly' => $_SESSION['readOnly'] ?? false,
|
||||
'isAdmin' => !empty($_SESSION['isAdmin']),
|
||||
'totp_enabled' => $totp,
|
||||
'username' => $_SESSION['username'],
|
||||
'folderOnly' => $_SESSION['folderOnly'] ?? false,
|
||||
'readOnly' => $_SESSION['readOnly'] ?? false,
|
||||
'disableUpload' => $_SESSION['disableUpload'] ?? false
|
||||
];
|
||||
|
||||
echo json_encode($resp);
|
||||
exit();
|
||||
}
|
||||
@@ -403,10 +449,19 @@ class AuthController
|
||||
*/
|
||||
public function getToken(): void
|
||||
{
|
||||
// 1) Ensure session and CSRF token exist
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
// 2) Emit headers
|
||||
header('Content-Type: application/json');
|
||||
header('X-CSRF-Token: ' . $_SESSION['csrf_token']);
|
||||
|
||||
// 3) Return JSON payload
|
||||
echo json_encode([
|
||||
"csrf_token" => $_SESSION['csrf_token'],
|
||||
"share_url" => SHARE_URL
|
||||
'csrf_token' => $_SESSION['csrf_token'],
|
||||
'share_url' => SHARE_URL
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
// src/controllers/folderController.php
|
||||
// src/controllers/FolderController.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/FolderModel.php';
|
||||
@@ -76,7 +76,11 @@ class FolderController
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to create folders."]);
|
||||
http_response_code(403);
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"error" => "Read-only users are not allowed to create folders."
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -401,6 +405,20 @@ class FolderController
|
||||
*
|
||||
* @return void Outputs HTML content.
|
||||
*/
|
||||
|
||||
function formatBytes($bytes)
|
||||
{
|
||||
if ($bytes < 1024) {
|
||||
return $bytes . " B";
|
||||
} elseif ($bytes < 1024 * 1024) {
|
||||
return round($bytes / 1024, 2) . " KB";
|
||||
} elseif ($bytes < 1024 * 1024 * 1024) {
|
||||
return round($bytes / (1024 * 1024), 2) . " MB";
|
||||
} else {
|
||||
return round($bytes / (1024 * 1024 * 1024), 2) . " GB";
|
||||
}
|
||||
}
|
||||
|
||||
public function shareFolder(): void
|
||||
{
|
||||
// Retrieve GET parameters.
|
||||
@@ -495,12 +513,14 @@ class FolderController
|
||||
exit;
|
||||
}
|
||||
|
||||
// Extract data for the HTML view.
|
||||
$folderName = $data['folder'];
|
||||
$files = $data['files'];
|
||||
$currentPage = $data['currentPage'];
|
||||
$totalPages = $data['totalPages'];
|
||||
// Load admin config so we can pull the sharedMaxUploadSize
|
||||
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||||
$adminConfig = AdminModel::getConfig();
|
||||
$sharedMaxUploadSize = isset($adminConfig['sharedMaxUploadSize']) && is_numeric($adminConfig['sharedMaxUploadSize'])
|
||||
? (int)$adminConfig['sharedMaxUploadSize']
|
||||
: null;
|
||||
|
||||
// For human‐readable formatting
|
||||
function formatBytes($bytes)
|
||||
{
|
||||
if ($bytes < 1024) {
|
||||
@@ -514,6 +534,12 @@ class FolderController
|
||||
}
|
||||
}
|
||||
|
||||
// Extract data for the HTML view.
|
||||
$folderName = $data['folder'];
|
||||
$files = $data['files'];
|
||||
$currentPage = $data['currentPage'];
|
||||
$totalPages = $data['totalPages'];
|
||||
|
||||
// Build the HTML view.
|
||||
header("Content-Type: text/html; charset=utf-8");
|
||||
?>
|
||||
@@ -528,13 +554,18 @@ class FolderController
|
||||
body {
|
||||
background: #f2f2f2;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
padding: 0px 20px 20px 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -639,6 +670,28 @@ class FolderController
|
||||
font-size: 0.9rem;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background-color: #007BFF;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.pagination span {
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -648,7 +701,7 @@ class FolderController
|
||||
</div>
|
||||
<div class="container">
|
||||
<!-- Toggle Button -->
|
||||
<button id="toggleBtn" class="toggle-btn" onclick="toggleViewMode()">Switch to Gallery View</button>
|
||||
<button id="toggleBtn" class="toggle-btn">Switch to Gallery View</button>
|
||||
|
||||
<!-- List View Container -->
|
||||
<div id="listViewContainer">
|
||||
@@ -717,7 +770,11 @@ class FolderController
|
||||
<!-- Upload Container (if uploads are allowed by the share record) -->
|
||||
<?php if (isset($data['record']['allowUpload']) && $data['record']['allowUpload'] == 1): ?>
|
||||
<div class="upload-container">
|
||||
<h3>Upload File (50mb max size)</h3>
|
||||
<h3>Upload File
|
||||
<?php if ($sharedMaxUploadSize !== null): ?>
|
||||
(<?php echo formatBytes($sharedMaxUploadSize); ?> max size)
|
||||
<?php endif; ?>
|
||||
</h3>
|
||||
<form action="/api/folder/uploadToSharedFolder.php" method="post" enctype="multipart/form-data">
|
||||
<!-- Pass the share token so the upload endpoint can verify -->
|
||||
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
@@ -731,75 +788,14 @@ class FolderController
|
||||
<div class="footer">
|
||||
© <?php echo date("Y"); ?> FileRise. All rights reserved.
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// (Optional) JavaScript for toggling view modes (list/gallery).
|
||||
var viewMode = 'list';
|
||||
window.imageCache = window.imageCache || {};
|
||||
var filesData = <?php echo json_encode($files); ?>;
|
||||
|
||||
// Use the shared‑folder relative path (from your model), not realFolderPath
|
||||
// $data['folder'] should be something like "eafwef/testfolder2/test/new folder two"
|
||||
var rawRelPath = "<?php echo addslashes($data['folder']); ?>";
|
||||
// Split into segments, encode each segment, then re-join
|
||||
var folderSegments = rawRelPath
|
||||
.split('/')
|
||||
.map(encodeURIComponent)
|
||||
.join('/');
|
||||
|
||||
function renderGalleryView() {
|
||||
var galleryContainer = document.getElementById("galleryViewContainer");
|
||||
var html = '<div class="shared-gallery-container">';
|
||||
filesData.forEach(function(file) {
|
||||
// Encode the filename too
|
||||
var fileName = encodeURIComponent(file);
|
||||
var fileUrl = window.location.origin +
|
||||
'/uploads/' +
|
||||
folderSegments +
|
||||
'/' +
|
||||
fileName +
|
||||
'?t=' +
|
||||
Date.now();
|
||||
|
||||
var ext = file.split('.').pop().toLowerCase();
|
||||
var thumbnail;
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].indexOf(ext) >= 0) {
|
||||
thumbnail = '<img src="' + fileUrl + '" alt="' + file + '">';
|
||||
} else {
|
||||
thumbnail = '<span class="material-icons">insert_drive_file</span>';
|
||||
}
|
||||
|
||||
html +=
|
||||
'<div class="shared-gallery-card">' +
|
||||
'<div class="gallery-preview" ' +
|
||||
'onclick="window.location.href=\'' + fileUrl + '\'" ' +
|
||||
'style="cursor:pointer;">' +
|
||||
thumbnail +
|
||||
'</div>' +
|
||||
'<div class="gallery-info">' +
|
||||
'<span class="gallery-file-name">' + file + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
galleryContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
function toggleViewMode() {
|
||||
if (viewMode === 'list') {
|
||||
viewMode = 'gallery';
|
||||
document.getElementById("listViewContainer").style.display = "none";
|
||||
renderGalleryView();
|
||||
document.getElementById("galleryViewContainer").style.display = "block";
|
||||
document.getElementById("toggleBtn").textContent = "Switch to List View";
|
||||
} else {
|
||||
viewMode = 'list';
|
||||
document.getElementById("galleryViewContainer").style.display = "none";
|
||||
document.getElementById("listViewContainer").style.display = "block";
|
||||
document.getElementById("toggleBtn").textContent = "Switch to Gallery View";
|
||||
}
|
||||
<!-- non-executing JSON payload, never blocked by CSP -->
|
||||
<script type="application/json" id="shared-data">
|
||||
{
|
||||
"token": <?php echo json_encode($token, JSON_HEX_TAG); ?>,
|
||||
"files": <?php echo json_encode($files, JSON_HEX_TAG); ?>
|
||||
}
|
||||
</script>
|
||||
<script src="/js/sharedFolderView.js" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -855,38 +851,63 @@ class FolderController
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
// Auth check
|
||||
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check that the user is not read-only.
|
||||
// Read-only check
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
$perms = loadUserPermissions($username);
|
||||
if ($username && !empty($perms['readOnly'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Read-only users are not allowed to create share folders."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Retrieve and decode POST input.
|
||||
$input = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$input || !isset($input['folder'])) {
|
||||
// Input
|
||||
$in = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$in || !isset($in['folder'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = trim($input['folder']);
|
||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
||||
$password = isset($input['password']) ? $input['password'] : "";
|
||||
$allowUpload = isset($input['allowUpload']) ? intval($input['allowUpload']) : 0;
|
||||
$folder = trim($in['folder']);
|
||||
$value = isset($in['expirationValue']) ? intval($in['expirationValue']) : 60;
|
||||
$unit = $in['expirationUnit'] ?? 'minutes';
|
||||
$password = $in['password'] ?? '';
|
||||
$allowUpload = intval($in['allowUpload'] ?? 0);
|
||||
|
||||
// Delegate to the model.
|
||||
$result = FolderModel::createShareFolderLink($folder, $expirationMinutes, $password, $allowUpload);
|
||||
echo json_encode($result);
|
||||
// Folder name validation
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
switch ($unit) {
|
||||
case 'seconds':
|
||||
$seconds = $value;
|
||||
break;
|
||||
case 'hours':
|
||||
$seconds = $value * 3600;
|
||||
break;
|
||||
case 'days':
|
||||
$seconds = $value * 86400;
|
||||
break;
|
||||
case 'minutes':
|
||||
default:
|
||||
$seconds = $value * 60;
|
||||
break;
|
||||
}
|
||||
|
||||
// Delegate
|
||||
$res = FolderModel::createShareFolderLink($folder, $seconds, $password, $allowUpload);
|
||||
echo json_encode($res);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -1057,4 +1078,53 @@ class FolderController
|
||||
header("Location: " . $redirectUrl);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/folder/getShareFolderLinks.php
|
||||
*/
|
||||
public function getAllShareFolderLinks(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
$shareFile = META_DIR . 'share_folder_links.json';
|
||||
$links = file_exists($shareFile)
|
||||
? json_decode(file_get_contents($shareFile), true) ?? []
|
||||
: [];
|
||||
$now = time();
|
||||
$cleaned = [];
|
||||
|
||||
// 1) Remove expired
|
||||
foreach ($links as $token => $record) {
|
||||
if (!empty($record['expires']) && $record['expires'] < $now) {
|
||||
continue;
|
||||
}
|
||||
$cleaned[$token] = $record;
|
||||
}
|
||||
|
||||
// 2) Persist back if anything was pruned
|
||||
if (count($cleaned) !== count($links)) {
|
||||
file_put_contents($shareFile, json_encode($cleaned, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
echo json_encode($cleaned);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/folder/deleteShareFolderLink.php
|
||||
*/
|
||||
public function deleteShareFolderLink()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
$token = $_POST['token'] ?? '';
|
||||
if (!$token) {
|
||||
echo json_encode(['success' => false, 'error' => 'No token provided']);
|
||||
return;
|
||||
}
|
||||
|
||||
$deleted = FolderModel::deleteShareFolderLink($token);
|
||||
if ($deleted) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Not found']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
// src/controllers/uploadController.php
|
||||
// src/controllers/UploadController.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/UploadModel.php';
|
||||
@@ -72,34 +72,56 @@ class UploadController {
|
||||
*/
|
||||
public function handleUpload(): void {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// CSRF Protection.
|
||||
|
||||
//
|
||||
// 1) CSRF – pull from header or POST fields
|
||||
//
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = $headersArr['x-csrf-token'] ?? '';
|
||||
if (!isset($_SESSION['csrf_token']) || trim($receivedToken) !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
$received = '';
|
||||
if (!empty($headersArr['x-csrf-token'])) {
|
||||
$received = trim($headersArr['x-csrf-token']);
|
||||
} elseif (!empty($_POST['csrf_token'])) {
|
||||
$received = trim($_POST['csrf_token']);
|
||||
} elseif (!empty($_POST['upload_token'])) {
|
||||
$received = trim($_POST['upload_token']);
|
||||
}
|
||||
|
||||
// 1a) If it doesn’t match, soft-fail: send new token and let client retry
|
||||
if (!isset($_SESSION['csrf_token']) || $received !== $_SESSION['csrf_token']) {
|
||||
// regenerate
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
// tell client “please retry with this new token”
|
||||
http_response_code(200);
|
||||
echo json_encode([
|
||||
'csrf_expired' => true,
|
||||
'csrf_token' => $_SESSION['csrf_token']
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
|
||||
//
|
||||
// 2) Auth checks
|
||||
//
|
||||
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
// Check user permissions.
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username && !empty($userPermissions['disableUpload'])) {
|
||||
$userPerms = loadUserPermissions($_SESSION['username']);
|
||||
if (!empty($userPerms['disableUpload'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Upload disabled for this user."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Delegate to the model.
|
||||
|
||||
//
|
||||
// 3) Delegate the actual file handling
|
||||
//
|
||||
$result = UploadModel::handleUpload($_POST, $_FILES);
|
||||
|
||||
// For chunked uploads, output JSON (e.g., "chunk uploaded" status).
|
||||
|
||||
//
|
||||
// 4) Respond
|
||||
//
|
||||
if (isset($result['error'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode($result);
|
||||
@@ -109,8 +131,8 @@ class UploadController {
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Otherwise, for full upload success, set a flash message and redirect.
|
||||
|
||||
// full‐upload redirect
|
||||
$_SESSION['upload_message'] = "File uploaded successfully.";
|
||||
exit;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
// userController.php located in src/controllers/
|
||||
// UserController.php located in src/controllers/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
||||
@@ -87,63 +87,83 @@ class UserController
|
||||
|
||||
public function addUser()
|
||||
{
|
||||
// 1) Ensure JSON output and session
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
// 1a) Initialize CSRF token if missing
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
// Determine if we're in setup mode.
|
||||
// Setup mode means the "setup" query parameter is passed
|
||||
// and users.txt is missing, empty, or contains only whitespace.
|
||||
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
|
||||
if ($isSetup && (!file_exists($usersFile) || filesize($usersFile) == 0 || trim(file_get_contents($usersFile)) === '')) {
|
||||
// Allow initial admin creation without session or CSRF checks.
|
||||
// 2) Determine setup mode (first-ever admin creation)
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
|
||||
$setupMode = false;
|
||||
if (
|
||||
$isSetup && (! file_exists($usersFile)
|
||||
|| filesize($usersFile) === 0
|
||||
|| trim(file_get_contents($usersFile)) === ''
|
||||
)
|
||||
) {
|
||||
$setupMode = true;
|
||||
} else {
|
||||
$setupMode = false;
|
||||
// In non-setup mode, perform CSRF token and authentication checks.
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
||||
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
// 3) In non-setup, enforce CSRF + auth checks
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = trim($headersArr['x-csrf-token'] ?? '');
|
||||
|
||||
// 3a) Soft-fail CSRF: on mismatch, regenerate and return new token
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
header('X-CSRF-Token: ' . $_SESSION['csrf_token']);
|
||||
echo json_encode([
|
||||
'csrf_expired' => true,
|
||||
'csrf_token' => $_SESSION['csrf_token']
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3b) Must be logged in as admin
|
||||
if (
|
||||
!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
||||
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true
|
||||
empty($_SESSION['authenticated'])
|
||||
|| $_SESSION['authenticated'] !== true
|
||||
|| empty($_SESSION['isAdmin'])
|
||||
|| $_SESSION['isAdmin'] !== true
|
||||
) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the JSON input data.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
$newUsername = trim($data["username"] ?? "");
|
||||
$newPassword = trim($data["password"] ?? "");
|
||||
// 4) Parse input
|
||||
$data = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||
$newUsername = trim($data['username'] ?? '');
|
||||
$newPassword = trim($data['password'] ?? '');
|
||||
|
||||
// In setup mode, force the new user to be an admin.
|
||||
// 5) Determine admin flag
|
||||
if ($setupMode) {
|
||||
$isAdmin = "1";
|
||||
$isAdmin = '1';
|
||||
} else {
|
||||
$isAdmin = !empty($data["isAdmin"]) ? "1" : "0";
|
||||
$isAdmin = !empty($data['isAdmin']) ? '1' : '0';
|
||||
}
|
||||
|
||||
// Validate that a username and password are provided.
|
||||
if (!$newUsername || !$newPassword) {
|
||||
// 6) Validate fields
|
||||
if ($newUsername === '' || $newPassword === '') {
|
||||
echo json_encode(["error" => "Username and password required"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate username format.
|
||||
if (!preg_match(REGEX_USER, $newUsername)) {
|
||||
echo json_encode(["error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
||||
echo json_encode([
|
||||
"error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Delegate the business logic to the model.
|
||||
// 7) Delegate to model
|
||||
$result = userModel::addUser($newUsername, $newPassword, $isAdmin, $setupMode);
|
||||
|
||||
// 8) Return model result
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -852,7 +872,7 @@ class UserController
|
||||
header('Content-Type: application/json');
|
||||
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
||||
|
||||
// Rate‑limit
|
||||
// Rate-limit
|
||||
if (!isset($_SESSION['totp_failures'])) {
|
||||
$_SESSION['totp_failures'] = 0;
|
||||
}
|
||||
@@ -863,7 +883,7 @@ class UserController
|
||||
}
|
||||
|
||||
// Must be authenticated OR pending login
|
||||
if (!((!empty($_SESSION['authenticated'])) || isset($_SESSION['pending_login_user']))) {
|
||||
if (empty($_SESSION['authenticated']) && !isset($_SESSION['pending_login_user'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
|
||||
exit;
|
||||
@@ -878,7 +898,7 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
// Parse and validate input
|
||||
// Parse & validate input
|
||||
$inputData = json_decode(file_get_contents("php://input"), true);
|
||||
$code = trim($inputData['totp_code'] ?? '');
|
||||
if (!preg_match('/^\d{6}$/', $code)) {
|
||||
@@ -893,11 +913,11 @@ class UserController
|
||||
'FileRise', 6, 30, \RobThree\Auth\Algorithm::Sha1
|
||||
);
|
||||
|
||||
// Pending‑login flow (first password step passed)
|
||||
// === Pending-login flow (we just came from auth and need to finish login) ===
|
||||
if (isset($_SESSION['pending_login_user'])) {
|
||||
$username = $_SESSION['pending_login_user'];
|
||||
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
|
||||
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
|
||||
$username = $_SESSION['pending_login_user'];
|
||||
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
|
||||
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
|
||||
|
||||
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
|
||||
$_SESSION['totp_failures']++;
|
||||
@@ -906,53 +926,45 @@ class UserController
|
||||
exit;
|
||||
}
|
||||
|
||||
// === Issue “remember me” token if requested ===
|
||||
// Issue “remember me” token if requested
|
||||
if ($rememberMe) {
|
||||
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiry = time() + 30 * 24 * 60 * 60;
|
||||
$all = [];
|
||||
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiry = time() + 30 * 24 * 60 * 60;
|
||||
$all = [];
|
||||
if (file_exists($tokFile)) {
|
||||
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||
$all = json_decode($dec, true) ?: [];
|
||||
}
|
||||
$all[$token] = [
|
||||
'username' => $username,
|
||||
'expiry' => $expiry,
|
||||
'isAdmin' => $_SESSION['isAdmin']
|
||||
'username' => $username,
|
||||
'expiry' => $expiry,
|
||||
'isAdmin' => ((int)userModel::getUserRole($username) === 1),
|
||||
'folderOnly' => loadUserPermissions($username)['folderOnly'] ?? false,
|
||||
'readOnly' => loadUserPermissions($username)['readOnly'] ?? false,
|
||||
'disableUpload'=> loadUserPermissions($username)['disableUpload']?? false
|
||||
];
|
||||
file_put_contents(
|
||||
$tokFile,
|
||||
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||
LOCK_EX
|
||||
);
|
||||
|
||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
|
||||
// Persistent cookie
|
||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||
|
||||
// Re‑issue PHP session cookie
|
||||
setcookie(
|
||||
session_name(),
|
||||
session_id(),
|
||||
$expiry,
|
||||
'/',
|
||||
'',
|
||||
$secure,
|
||||
true
|
||||
);
|
||||
setcookie(session_name(), session_id(), $expiry, '/', '', $secure, true);
|
||||
}
|
||||
|
||||
// Finalize login
|
||||
// === Finalize login into session exactly as finalizeLogin() would ===
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['authenticated'] = true;
|
||||
$_SESSION['username'] = $username;
|
||||
$_SESSION['isAdmin'] = (userModel::getUserRole($username) === "1");
|
||||
$_SESSION['folderOnly'] = loadUserPermissions($username);
|
||||
$_SESSION['authenticated'] = true;
|
||||
$_SESSION['username'] = $username;
|
||||
$_SESSION['isAdmin'] = ((int)userModel::getUserRole($username) === 1);
|
||||
$perms = loadUserPermissions($username);
|
||||
$_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
|
||||
$_SESSION['readOnly'] = $perms['readOnly'] ?? false;
|
||||
$_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
|
||||
|
||||
// Clean up
|
||||
// Clean up pending markers
|
||||
unset(
|
||||
$_SESSION['pending_login_user'],
|
||||
$_SESSION['pending_login_secret'],
|
||||
@@ -960,34 +972,43 @@ class UserController
|
||||
$_SESSION['totp_failures']
|
||||
);
|
||||
|
||||
echo json_encode(['status' => 'ok', 'message' => 'Login successful']);
|
||||
// Send back full login payload
|
||||
echo json_encode([
|
||||
'status' => 'ok',
|
||||
'success' => 'Login successful',
|
||||
'isAdmin' => $_SESSION['isAdmin'],
|
||||
'folderOnly' => $_SESSION['folderOnly'],
|
||||
'readOnly' => $_SESSION['readOnly'],
|
||||
'disableUpload' => $_SESSION['disableUpload'],
|
||||
'username' => $_SESSION['username']
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Setup/verification flow (not pending)
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if (!$username) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Username not found in session']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$totpSecret = userModel::getTOTPSecret($username);
|
||||
if (!$totpSecret) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$tfa->verifyCode($totpSecret, $code)) {
|
||||
$_SESSION['totp_failures']++;
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Successful setup/verification
|
||||
unset($_SESSION['totp_failures']);
|
||||
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
||||
}
|
||||
|
||||
// Setup/verification flow (not pending)
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if (!$username) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Username not found in session']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$totpSecret = userModel::getTOTPSecret($username);
|
||||
if (!$totpSecret) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$tfa->verifyCode($totpSecret, $code)) {
|
||||
$_SESSION['totp_failures']++;
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Successful setup/verification
|
||||
unset($_SESSION['totp_failures']);
|
||||
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,23 @@ require_once PROJECT_ROOT . '/config/config.php';
|
||||
|
||||
class AdminModel
|
||||
{
|
||||
/**
|
||||
* Parse a shorthand size value (e.g. "5G", "500M", "123K") into bytes.
|
||||
*
|
||||
* @param string $val
|
||||
* @return int
|
||||
*/
|
||||
private static function parseSize(string $val): int
|
||||
{
|
||||
$unit = strtolower(substr($val, -1));
|
||||
$num = (int) rtrim($val, 'bkmgtpezyBKMGTPESY');
|
||||
switch ($unit) {
|
||||
case 'g': return $num * 1024 ** 3;
|
||||
case 'm': return $num * 1024 ** 2;
|
||||
case 'k': return $num * 1024;
|
||||
default: return $num;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the admin configuration file.
|
||||
@@ -24,6 +41,28 @@ class AdminModel
|
||||
return ["error" => "Incomplete OIDC configuration."];
|
||||
}
|
||||
|
||||
// Ensure enableWebDAV flag is boolean (default to false if missing)
|
||||
$configUpdate['enableWebDAV'] = isset($configUpdate['enableWebDAV'])
|
||||
? (bool)$configUpdate['enableWebDAV']
|
||||
: false;
|
||||
|
||||
// Validate sharedMaxUploadSize if provided
|
||||
if (isset($configUpdate['sharedMaxUploadSize'])) {
|
||||
$sms = filter_var(
|
||||
$configUpdate['sharedMaxUploadSize'],
|
||||
FILTER_VALIDATE_INT,
|
||||
["options" => ["min_range" => 1]]
|
||||
);
|
||||
if ($sms === false) {
|
||||
return ["error" => "Invalid sharedMaxUploadSize."];
|
||||
}
|
||||
$totalBytes = self::parseSize(TOTAL_UPLOAD_SIZE);
|
||||
if ($sms > $totalBytes) {
|
||||
return ["error" => "sharedMaxUploadSize must be ≤ TOTAL_UPLOAD_SIZE."];
|
||||
}
|
||||
$configUpdate['sharedMaxUploadSize'] = $sms;
|
||||
}
|
||||
|
||||
// Convert configuration to JSON.
|
||||
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
||||
if ($plainTextConfig === false) {
|
||||
@@ -59,7 +98,8 @@ class AdminModel
|
||||
*
|
||||
* @return array The configuration array, or defaults if not found.
|
||||
*/
|
||||
public static function getConfig(): array {
|
||||
public static function getConfig(): array
|
||||
{
|
||||
$configFile = USERS_DIR . 'adminConfig.json';
|
||||
if (file_exists($configFile)) {
|
||||
$encryptedContent = file_get_contents($configFile);
|
||||
@@ -72,10 +112,9 @@ class AdminModel
|
||||
if (!is_array($config)) {
|
||||
$config = [];
|
||||
}
|
||||
|
||||
// Normalize login options.
|
||||
|
||||
// Normalize login options if missing
|
||||
if (!isset($config['loginOptions'])) {
|
||||
// Create loginOptions array from top-level keys if missing.
|
||||
$config['loginOptions'] = [
|
||||
'disableFormLogin' => isset($config['disableFormLogin']) ? (bool)$config['disableFormLogin'] : false,
|
||||
'disableBasicAuth' => isset($config['disableBasicAuth']) ? (bool)$config['disableBasicAuth'] : false,
|
||||
@@ -88,31 +127,43 @@ class AdminModel
|
||||
$config['loginOptions']['disableBasicAuth'] = (bool)$config['loginOptions']['disableBasicAuth'];
|
||||
$config['loginOptions']['disableOIDCLogin'] = (bool)$config['loginOptions']['disableOIDCLogin'];
|
||||
}
|
||||
|
||||
|
||||
// Default values for other keys
|
||||
if (!isset($config['globalOtpauthUrl'])) {
|
||||
$config['globalOtpauthUrl'] = "";
|
||||
}
|
||||
if (!isset($config['header_title']) || empty($config['header_title'])) {
|
||||
$config['header_title'] = "FileRise";
|
||||
}
|
||||
if (!isset($config['enableWebDAV'])) {
|
||||
$config['enableWebDAV'] = false;
|
||||
}
|
||||
// Default sharedMaxUploadSize to 50MB or TOTAL_UPLOAD_SIZE if smaller
|
||||
if (!isset($config['sharedMaxUploadSize'])) {
|
||||
$defaultSms = min(50 * 1024 * 1024, self::parseSize(TOTAL_UPLOAD_SIZE));
|
||||
$config['sharedMaxUploadSize'] = $defaultSms;
|
||||
}
|
||||
|
||||
return $config;
|
||||
} else {
|
||||
// Return defaults.
|
||||
return [
|
||||
'header_title' => "FileRise",
|
||||
'oidc' => [
|
||||
'header_title' => "FileRise",
|
||||
'oidc' => [
|
||||
'providerUrl' => 'https://your-oidc-provider.com',
|
||||
'clientId' => 'YOUR_CLIENT_ID',
|
||||
'clientSecret' => 'YOUR_CLIENT_SECRET',
|
||||
'redirectUri' => 'https://yourdomain.com/api/auth/auth.php?oidc=callback'
|
||||
],
|
||||
'loginOptions' => [
|
||||
'loginOptions' => [
|
||||
'disableFormLogin' => false,
|
||||
'disableBasicAuth' => false,
|
||||
'disableOIDCLogin' => false
|
||||
],
|
||||
'globalOtpauthUrl' => ""
|
||||
'globalOtpauthUrl' => "",
|
||||
'enableWebDAV' => false,
|
||||
'sharedMaxUploadSize' => min(50 * 1024 * 1024, self::parseSize(TOTAL_UPLOAD_SIZE))
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
require_once PROJECT_ROOT . '/config/config.php';
|
||||
|
||||
class AuthModel {
|
||||
class AuthModel
|
||||
{
|
||||
|
||||
/**
|
||||
* Retrieves the user's role from the users file.
|
||||
@@ -11,7 +12,8 @@ class AuthModel {
|
||||
* @param string $username
|
||||
* @return string|null The role string (e.g. "1" for admin) or null if not found.
|
||||
*/
|
||||
public static function getUserRole(string $username): ?string {
|
||||
public static function getUserRole(string $username): ?string
|
||||
{
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
if (file_exists($usersFile)) {
|
||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
@@ -23,7 +25,7 @@ class AuthModel {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Authenticates the user using form-based credentials.
|
||||
*
|
||||
@@ -31,7 +33,8 @@ class AuthModel {
|
||||
* @param string $password
|
||||
* @return array|false Returns an associative array with user data (role, totp_secret) on success or false on failure.
|
||||
*/
|
||||
public static function authenticate(string $username, string $password) {
|
||||
public static function authenticate(string $username, string $password)
|
||||
{
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
if (!file_exists($usersFile)) {
|
||||
return false;
|
||||
@@ -51,14 +54,15 @@ class AuthModel {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads failed login attempts from a file.
|
||||
*
|
||||
* @param string $file
|
||||
* @return array
|
||||
*/
|
||||
public static function loadFailedAttempts(string $file): array {
|
||||
public static function loadFailedAttempts(string $file): array
|
||||
{
|
||||
if (file_exists($file)) {
|
||||
$data = json_decode(file_get_contents($file), true);
|
||||
if (is_array($data)) {
|
||||
@@ -67,7 +71,7 @@ class AuthModel {
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Saves failed login attempts into a file.
|
||||
*
|
||||
@@ -75,17 +79,19 @@ class AuthModel {
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public static function saveFailedAttempts(string $file, array $data): void {
|
||||
public static function saveFailedAttempts(string $file, array $data): void
|
||||
{
|
||||
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves a user's TOTP secret from the users file.
|
||||
*
|
||||
* @param string $username
|
||||
* @return string|null Returns the decrypted TOTP secret or null if not set.
|
||||
*/
|
||||
public static function getUserTOTPSecret(string $username): ?string {
|
||||
public static function getUserTOTPSecret(string $username): ?string
|
||||
{
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
if (!file_exists($usersFile)) {
|
||||
return null;
|
||||
@@ -98,14 +104,15 @@ class AuthModel {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads the folder-only permission for a given user.
|
||||
*
|
||||
* @param string $username
|
||||
* @return bool
|
||||
*/
|
||||
public static function loadFolderPermission(string $username): bool {
|
||||
public static function loadFolderPermission(string $username): bool
|
||||
{
|
||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||
if (file_exists($permissionsFile)) {
|
||||
$content = file_get_contents($permissionsFile);
|
||||
@@ -121,4 +128,31 @@ class AuthModel {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a remember-me token and return its stored payload.
|
||||
*
|
||||
* @param string $token
|
||||
* @return array|null Returns ['username'=>…, 'expiry'=>…, 'isAdmin'=>…] or null if invalid/expired.
|
||||
*/
|
||||
public static function validateRememberToken(string $token): ?array
|
||||
{
|
||||
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||
if (! file_exists($tokFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Decrypt and decode the full token store
|
||||
$encrypted = file_get_contents($tokFile);
|
||||
$json = decryptData($encrypted, $GLOBALS['encryptionKey']);
|
||||
$all = json_decode($json, true) ?: [];
|
||||
|
||||
// Lookup and expiry check
|
||||
if (empty($all[$token]) || !isset($all[$token]['expiry']) || $all[$token]['expiry'] < time()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Valid token—return its payload
|
||||
return $all[$token];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,7 +736,7 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
||||
* @return array Returns an associative array with keys "token" and "expires" on success,
|
||||
* or "error" on failure.
|
||||
*/
|
||||
public static function createShareLink($folder, $file, $expirationMinutes = 60, $password = "") {
|
||||
public static function createShareLink($folder, $file, $expirationSeconds = 3600, $password = "") {
|
||||
// Validate folder if necessary (this can also be done in the controller).
|
||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
@@ -746,7 +746,7 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
||||
$token = bin2hex(random_bytes(16));
|
||||
|
||||
// Calculate expiration (Unix timestamp).
|
||||
$expires = time() + ($expirationMinutes * 60);
|
||||
$expires = time() + $expirationSeconds;
|
||||
|
||||
// Hash the password if provided.
|
||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
@@ -1253,4 +1253,29 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
||||
|
||||
return ["files" => $fileList, "globalTags" => $globalTags];
|
||||
}
|
||||
|
||||
public static function getAllShareLinks(): array
|
||||
{
|
||||
$shareFile = META_DIR . "share_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
return [];
|
||||
}
|
||||
$links = json_decode(file_get_contents($shareFile), true);
|
||||
return is_array($links) ? $links : [];
|
||||
}
|
||||
|
||||
public static function deleteShareLink(string $token): bool
|
||||
{
|
||||
$shareFile = META_DIR . "share_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
return false;
|
||||
}
|
||||
$links = json_decode(file_get_contents($shareFile), true);
|
||||
if (!is_array($links) || !isset($links[$token])) {
|
||||
return false;
|
||||
}
|
||||
unset($links[$token]);
|
||||
file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
require_once PROJECT_ROOT . '/config/config.php';
|
||||
|
||||
class FolderModel {
|
||||
class FolderModel
|
||||
{
|
||||
/**
|
||||
* Creates a folder under the specified parent (or in root) and creates an empty metadata file.
|
||||
*
|
||||
@@ -12,10 +13,11 @@ class FolderModel {
|
||||
* @return array Returns an array with a "success" key if the folder was created,
|
||||
* or an "error" key if an error occurred.
|
||||
*/
|
||||
public static function createFolder(string $folderName, string $parent = ""): array {
|
||||
public static function createFolder(string $folderName, string $parent = ""): array
|
||||
{
|
||||
$folderName = trim($folderName);
|
||||
$parent = trim($parent);
|
||||
|
||||
|
||||
// Validate folder name (only letters, numbers, underscores, dashes, and spaces allowed).
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
@@ -23,7 +25,7 @@ class FolderModel {
|
||||
if ($parent !== "" && !preg_match(REGEX_FOLDER_NAME, $parent)) {
|
||||
return ["error" => "Invalid parent folder name."];
|
||||
}
|
||||
|
||||
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
if ($parent !== "" && strtolower($parent) !== "root") {
|
||||
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $parent . DIRECTORY_SEPARATOR . $folderName;
|
||||
@@ -32,12 +34,12 @@ class FolderModel {
|
||||
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $folderName;
|
||||
$relativePath = $folderName;
|
||||
}
|
||||
|
||||
|
||||
// Check if the folder already exists.
|
||||
if (file_exists($fullPath)) {
|
||||
return ["error" => "Folder already exists."];
|
||||
}
|
||||
|
||||
|
||||
// Attempt to create the folder.
|
||||
if (mkdir($fullPath, 0755, true)) {
|
||||
// Create an empty metadata file for the new folder.
|
||||
@@ -50,52 +52,54 @@ class FolderModel {
|
||||
return ["error" => "Failed to create folder."];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates the metadata file path for a given folder.
|
||||
*
|
||||
* @param string $folder The relative folder path.
|
||||
* @return string The metadata file path.
|
||||
*/
|
||||
private static function getMetadataFilePath(string $folder): string {
|
||||
private static function getMetadataFilePath(string $folder): string
|
||||
{
|
||||
if (strtolower($folder) === 'root' || trim($folder) === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Deletes a folder if it is empty and removes its corresponding metadata.
|
||||
*
|
||||
* @param string $folder The folder name (relative to the upload directory).
|
||||
* @return array An associative array with "success" on success or "error" on failure.
|
||||
*/
|
||||
public static function deleteFolder(string $folder): array {
|
||||
public static function deleteFolder(string $folder): array
|
||||
{
|
||||
// Prevent deletion of "root".
|
||||
if (strtolower($folder) === 'root') {
|
||||
return ["error" => "Cannot delete root folder."];
|
||||
}
|
||||
|
||||
|
||||
// Validate folder name.
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
}
|
||||
|
||||
|
||||
// Build the full folder path.
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $folder;
|
||||
|
||||
|
||||
// Check if the folder exists and is a directory.
|
||||
if (!file_exists($folderPath) || !is_dir($folderPath)) {
|
||||
return ["error" => "Folder does not exist."];
|
||||
}
|
||||
|
||||
|
||||
// Prevent deletion if the folder is not empty.
|
||||
$items = array_diff(scandir($folderPath), array('.', '..'));
|
||||
if (count($items) > 0) {
|
||||
return ["error" => "Folder is not empty."];
|
||||
}
|
||||
|
||||
|
||||
// Attempt to delete the folder.
|
||||
if (rmdir($folderPath)) {
|
||||
// Remove corresponding metadata file.
|
||||
@@ -109,43 +113,45 @@ class FolderModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Renames a folder and updates related metadata files.
|
||||
*
|
||||
* @param string $oldFolder The current folder name (relative to UPLOAD_DIR).
|
||||
* @param string $newFolder The new folder name.
|
||||
* @return array Returns an associative array with "success" on success or "error" on failure.
|
||||
*/
|
||||
public static function renameFolder(string $oldFolder, string $newFolder): array {
|
||||
public static function renameFolder(string $oldFolder, string $newFolder): array
|
||||
{
|
||||
// Sanitize and trim folder names.
|
||||
$oldFolder = trim($oldFolder, "/\\ ");
|
||||
$newFolder = trim($newFolder, "/\\ ");
|
||||
|
||||
|
||||
// Validate folder names.
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) {
|
||||
return ["error" => "Invalid folder name(s)."];
|
||||
}
|
||||
|
||||
|
||||
// Build the full folder paths.
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$oldPath = $baseDir . DIRECTORY_SEPARATOR . $oldFolder;
|
||||
$newPath = $baseDir . DIRECTORY_SEPARATOR . $newFolder;
|
||||
|
||||
|
||||
// Validate that the old folder exists and new folder does not.
|
||||
if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) ||
|
||||
strpos(realpath($oldPath), realpath($baseDir)) !== 0 ||
|
||||
strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0) {
|
||||
strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0
|
||||
) {
|
||||
return ["error" => "Invalid folder path."];
|
||||
}
|
||||
|
||||
|
||||
if (!file_exists($oldPath) || !is_dir($oldPath)) {
|
||||
return ["error" => "Folder to rename does not exist."];
|
||||
}
|
||||
|
||||
|
||||
if (file_exists($newPath)) {
|
||||
return ["error" => "New folder name already exists."];
|
||||
}
|
||||
|
||||
|
||||
// Attempt to rename the folder.
|
||||
if (rename($oldPath, $newPath)) {
|
||||
// Update metadata: Rename all metadata files that have the old folder prefix.
|
||||
@@ -171,7 +177,8 @@ class FolderModel {
|
||||
* @param string $relative The relative path from the base directory.
|
||||
* @return array An array of folder paths (relative to the base).
|
||||
*/
|
||||
private static function getSubfolders(string $dir, string $relative = ''): array {
|
||||
private static function getSubfolders(string $dir, string $relative = ''): array
|
||||
{
|
||||
$folders = [];
|
||||
$items = scandir($dir);
|
||||
$safeFolderNamePattern = REGEX_FOLDER_NAME;
|
||||
@@ -198,7 +205,8 @@ class FolderModel {
|
||||
*
|
||||
* @return array An array of folder information arrays.
|
||||
*/
|
||||
public static function getFolderList(): array {
|
||||
public static function getFolderList(): array
|
||||
{
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$folderInfoList = [];
|
||||
|
||||
@@ -240,13 +248,14 @@ class FolderModel {
|
||||
return $folderInfoList;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Retrieves the share folder record for a given token.
|
||||
*
|
||||
* @param string $token The share folder token.
|
||||
* @return array|null The share folder record, or null if not found.
|
||||
*/
|
||||
public static function getShareFolderRecord(string $token): ?array {
|
||||
public static function getShareFolderRecord(string $token): ?array
|
||||
{
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
return null;
|
||||
@@ -257,8 +266,8 @@ class FolderModel {
|
||||
}
|
||||
return $shareLinks[$token];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Retrieves shared folder data based on a share token.
|
||||
*
|
||||
* @param string $token The share folder token.
|
||||
@@ -274,7 +283,8 @@ class FolderModel {
|
||||
* - 'totalPages': total pages,
|
||||
* or an 'error' key on failure.
|
||||
*/
|
||||
public static function getSharedFolderData(string $token, ?string $providedPass, int $page = 1, int $itemsPerPage = 10): array {
|
||||
public static function getSharedFolderData(string $token, ?string $providedPass, int $page = 1, int $itemsPerPage = 10): array
|
||||
{
|
||||
// Load the share folder record.
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
@@ -314,7 +324,7 @@ class FolderModel {
|
||||
return ["error" => "Shared folder not found."];
|
||||
}
|
||||
// Scan for files (only files).
|
||||
$allFiles = array_values(array_filter(scandir($realFolderPath), function($item) use ($realFolderPath) {
|
||||
$allFiles = array_values(array_filter(scandir($realFolderPath), function ($item) use ($realFolderPath) {
|
||||
return is_file($realFolderPath . DIRECTORY_SEPARATOR . $item);
|
||||
}));
|
||||
sort($allFiles);
|
||||
@@ -323,7 +333,7 @@ class FolderModel {
|
||||
$currentPage = min($page, $totalPages);
|
||||
$startIndex = ($currentPage - 1) * $itemsPerPage;
|
||||
$filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage);
|
||||
|
||||
|
||||
return [
|
||||
"record" => $record,
|
||||
"folder" => $folder,
|
||||
@@ -334,81 +344,72 @@ class FolderModel {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Creates a share link for a folder.
|
||||
*
|
||||
* @param string $folder The folder to share (relative to UPLOAD_DIR).
|
||||
* @param int $expirationMinutes The duration (in minutes) until the link expires.
|
||||
* @param string $password Optional password for the share.
|
||||
* @param int $allowUpload Optional flag (0 or 1) indicating whether uploads are allowed.
|
||||
* @return array An associative array with "token", "expires", and "link" on success, or "error" on failure.
|
||||
* @param string $folder The folder to share (relative to UPLOAD_DIR).
|
||||
* @param int $expirationSeconds How many seconds until expiry.
|
||||
* @param string $password Optional password.
|
||||
* @param int $allowUpload 0 or 1 whether uploads are allowed.
|
||||
* @return array ["token","expires","link"] on success, or ["error"].
|
||||
*/
|
||||
public static function createShareFolderLink(string $folder, int $expirationMinutes = 60, string $password = "", int $allowUpload = 0): array {
|
||||
// Validate folder name.
|
||||
public static function createShareFolderLink(string $folder, int $expirationSeconds = 3600, string $password = "", int $allowUpload = 0): array
|
||||
{
|
||||
// Validate folder
|
||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
}
|
||||
|
||||
// Generate secure token.
|
||||
// Token
|
||||
try {
|
||||
$token = bin2hex(random_bytes(16)); // 32 hex characters.
|
||||
$token = bin2hex(random_bytes(16));
|
||||
} catch (Exception $e) {
|
||||
return ["error" => "Could not generate token."];
|
||||
}
|
||||
|
||||
// Calculate expiration time.
|
||||
$expires = time() + ($expirationMinutes * 60);
|
||||
// Expiry
|
||||
$expires = time() + $expirationSeconds;
|
||||
|
||||
// Hash the password if provided.
|
||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
// Password hash
|
||||
$hashedPassword = $password !== "" ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
|
||||
// Define the share folder links file.
|
||||
// Load existing
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
$shareLinks = [];
|
||||
if (file_exists($shareFile)) {
|
||||
$data = file_get_contents($shareFile);
|
||||
$shareLinks = json_decode($data, true);
|
||||
if (!is_array($shareLinks)) {
|
||||
$shareLinks = [];
|
||||
$links = file_exists($shareFile)
|
||||
? json_decode(file_get_contents($shareFile), true) ?? []
|
||||
: [];
|
||||
|
||||
// Cleanup
|
||||
$now = time();
|
||||
foreach ($links as $k => $v) {
|
||||
if (!empty($v['expires']) && $v['expires'] < $now) {
|
||||
unset($links[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up expired share links.
|
||||
$currentTime = time();
|
||||
foreach ($shareLinks as $key => $link) {
|
||||
if (isset($link["expires"]) && $link["expires"] < $currentTime) {
|
||||
unset($shareLinks[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new share record.
|
||||
$shareLinks[$token] = [
|
||||
"folder" => $folder,
|
||||
"expires" => $expires,
|
||||
"password" => $hashedPassword,
|
||||
// Add new
|
||||
$links[$token] = [
|
||||
"folder" => $folder,
|
||||
"expires" => $expires,
|
||||
"password" => $hashedPassword,
|
||||
"allowUpload" => $allowUpload
|
||||
];
|
||||
|
||||
// Save the updated share links.
|
||||
if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT)) === false) {
|
||||
// Save
|
||||
if (file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT)) === false) {
|
||||
return ["error" => "Could not save share link."];
|
||||
}
|
||||
|
||||
// Determine the base URL.
|
||||
if (defined('BASE_URL') && !empty(BASE_URL) && strpos(BASE_URL, 'yourwebsite') === false) {
|
||||
$baseUrl = rtrim(BASE_URL, '/');
|
||||
} else {
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||
$host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : gethostbyname($_SERVER['SERVER_ADDR'] ?? 'localhost');
|
||||
$baseUrl = $protocol . "://" . $host;
|
||||
}
|
||||
// The share URL points to the shared folder page.
|
||||
$link = $baseUrl . "/api/folder/shareFolder.php?token=" . urlencode($token);
|
||||
|
||||
// Build URL
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||
$host = $_SERVER['HTTP_HOST'] ?? gethostbyname(gethostname());
|
||||
$baseUrl = $protocol . '://' . rtrim($host, '/');
|
||||
$link = $baseUrl . "/api/folder/shareFolder.php?token=" . urlencode($token);
|
||||
|
||||
return ["token" => $token, "expires" => $expires, "link" => $link];
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Retrieves information for a shared file from a shared folder link.
|
||||
*
|
||||
* @param string $token The share folder token.
|
||||
@@ -418,7 +419,8 @@ class FolderModel {
|
||||
* - "realFilePath": the absolute path to the file,
|
||||
* - "mimeType": the detected MIME type.
|
||||
*/
|
||||
public static function getSharedFileInfo(string $token, string $file): array {
|
||||
public static function getSharedFileInfo(string $token, string $file): array
|
||||
{
|
||||
// Load the share folder record.
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
@@ -457,14 +459,14 @@ class FolderModel {
|
||||
return ["error" => "Invalid file name."];
|
||||
}
|
||||
$file = basename($file);
|
||||
|
||||
|
||||
// Build the full file path.
|
||||
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
|
||||
$realFilePath = realpath($filePath);
|
||||
if ($realFilePath === false || strpos($realFilePath, $realFolderPath) !== 0 || !is_file($realFilePath)) {
|
||||
return ["error" => "File not found."];
|
||||
}
|
||||
|
||||
|
||||
$mimeType = mime_content_type($realFilePath);
|
||||
return [
|
||||
"realFilePath" => $realFilePath,
|
||||
@@ -479,11 +481,12 @@ class FolderModel {
|
||||
* @param array $fileUpload The $_FILES['fileToUpload'] array.
|
||||
* @return array An associative array with "success" on success or "error" on failure.
|
||||
*/
|
||||
public static function uploadToSharedFolder(string $token, array $fileUpload): array {
|
||||
public static function uploadToSharedFolder(string $token, array $fileUpload): array
|
||||
{
|
||||
// Define maximum file size and allowed extensions.
|
||||
$maxSize = 50 * 1024 * 1024; // 50 MB
|
||||
$allowedExtensions = ['jpg','jpeg','png','gif','pdf','doc','docx','txt','xls','xlsx','ppt','pptx','mp4','webm','mp3','mkv'];
|
||||
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'txt', 'xls', 'xlsx', 'ppt', 'pptx', 'mp4', 'webm', 'mp3', 'mkv'];
|
||||
|
||||
// Load the share folder record.
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
@@ -494,55 +497,55 @@ class FolderModel {
|
||||
return ["error" => "Invalid share token."];
|
||||
}
|
||||
$record = $shareLinks[$token];
|
||||
|
||||
|
||||
// Check expiration.
|
||||
if (time() > $record['expires']) {
|
||||
return ["error" => "This share link has expired."];
|
||||
}
|
||||
|
||||
|
||||
// Check whether uploads are allowed.
|
||||
if (empty($record['allowUpload']) || $record['allowUpload'] != 1) {
|
||||
return ["error" => "File uploads are not allowed for this share."];
|
||||
}
|
||||
|
||||
|
||||
// Validate file upload presence.
|
||||
if ($fileUpload['error'] !== UPLOAD_ERR_OK) {
|
||||
return ["error" => "File upload error. Code: " . $fileUpload['error']];
|
||||
}
|
||||
|
||||
|
||||
if ($fileUpload['size'] > $maxSize) {
|
||||
return ["error" => "File size exceeds allowed limit."];
|
||||
}
|
||||
|
||||
|
||||
$uploadedName = basename($fileUpload['name']);
|
||||
$ext = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $allowedExtensions)) {
|
||||
return ["error" => "File type not allowed."];
|
||||
}
|
||||
|
||||
|
||||
// Determine the target folder from the share record.
|
||||
$folderName = trim($record['folder'], "/\\");
|
||||
$targetFolder = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||
if (!empty($folderName) && strtolower($folderName) !== 'root') {
|
||||
$targetFolder .= $folderName;
|
||||
}
|
||||
|
||||
|
||||
// Verify target folder exists.
|
||||
$realTargetFolder = realpath($targetFolder);
|
||||
$uploadDirReal = realpath(UPLOAD_DIR);
|
||||
if ($realTargetFolder === false || strpos($realTargetFolder, $uploadDirReal) !== 0 || !is_dir($realTargetFolder)) {
|
||||
return ["error" => "Shared folder not found."];
|
||||
}
|
||||
|
||||
|
||||
// Generate a new filename (using uniqid and sanitizing the original name).
|
||||
$newFilename = uniqid() . "_" . preg_replace('/[^A-Za-z0-9_\-\.]/', '_', $uploadedName);
|
||||
$targetPath = $realTargetFolder . DIRECTORY_SEPARATOR . $newFilename;
|
||||
|
||||
|
||||
// Move the uploaded file.
|
||||
if (!move_uploaded_file($fileUpload['tmp_name'], $targetPath)) {
|
||||
return ["error" => "Failed to move the uploaded file."];
|
||||
}
|
||||
|
||||
|
||||
// --- Metadata Update ---
|
||||
// Determine metadata file.
|
||||
$metadataKey = (empty($folderName) || strtolower($folderName) === "root") ? "root" : $folderName;
|
||||
@@ -564,7 +567,32 @@ class FolderModel {
|
||||
"uploader" => $uploader
|
||||
];
|
||||
file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT));
|
||||
|
||||
|
||||
return ["success" => "File uploaded successfully.", "newFilename" => $newFilename];
|
||||
}
|
||||
}
|
||||
|
||||
public static function getAllShareFolderLinks(): array
|
||||
{
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
return [];
|
||||
}
|
||||
$links = json_decode(file_get_contents($shareFile), true);
|
||||
return is_array($links) ? $links : [];
|
||||
}
|
||||
|
||||
public static function deleteShareFolderLink(string $token): bool
|
||||
{
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
return false;
|
||||
}
|
||||
$links = json_decode(file_get_contents($shareFile), true);
|
||||
if (!is_array($links) || !isset($links[$token])) {
|
||||
return false;
|
||||
}
|
||||
unset($links[$token]);
|
||||
file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
194
start.sh
194
start.sh
@@ -1,162 +1,112 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
echo "🚀 Running start.sh..."
|
||||
|
||||
# Warn if default persistent tokens key is in use
|
||||
if [ "$PERSISTENT_TOKENS_KEY" = "default_please_change_this_key" ]; then
|
||||
echo "⚠️ WARNING: Using default persistent tokens key. Please override PERSISTENT_TOKENS_KEY for production."
|
||||
# 1) Token‐key warning
|
||||
if [ "${PERSISTENT_TOKENS_KEY}" = "default_please_change_this_key" ]; then
|
||||
echo "⚠️ WARNING: Using default persistent tokens key—override for production."
|
||||
fi
|
||||
|
||||
# Update config.php based on environment variables
|
||||
# 2) Update config.php based on environment variables
|
||||
CONFIG_FILE="/var/www/config/config.php"
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
echo "🔄 Updating config.php based on environment variables..."
|
||||
if [ -n "$TIMEZONE" ]; then
|
||||
echo " Setting TIMEZONE to $TIMEZONE"
|
||||
sed -i "s|define('TIMEZONE',[[:space:]]*'[^']*');|define('TIMEZONE', '$TIMEZONE');|" "$CONFIG_FILE"
|
||||
fi
|
||||
if [ -n "$DATE_TIME_FORMAT" ]; then
|
||||
echo "🔄 Setting DATE_TIME_FORMAT to $DATE_TIME_FORMAT"
|
||||
sed -i "s|define('DATE_TIME_FORMAT',[[:space:]]*'[^']*');|define('DATE_TIME_FORMAT', '$DATE_TIME_FORMAT');|" "$CONFIG_FILE"
|
||||
fi
|
||||
if [ -n "$TOTAL_UPLOAD_SIZE" ]; then
|
||||
echo "🔄 Setting TOTAL_UPLOAD_SIZE to $TOTAL_UPLOAD_SIZE"
|
||||
sed -i "s|define('TOTAL_UPLOAD_SIZE',[[:space:]]*'[^']*');|define('TOTAL_UPLOAD_SIZE', '$TOTAL_UPLOAD_SIZE');|" "$CONFIG_FILE"
|
||||
fi
|
||||
if [ -n "$SECURE" ]; then
|
||||
echo "🔄 Setting SECURE to $SECURE"
|
||||
sed -i "s|\$envSecure = getenv('SECURE');|\$envSecure = '$SECURE';|" "$CONFIG_FILE"
|
||||
fi
|
||||
if [ -n "$SHARE_URL" ]; then
|
||||
echo "🔄 Setting SHARE_URL to $SHARE_URL"
|
||||
sed -i "s|define('SHARE_URL',[[:space:]]*'[^']*');|define('SHARE_URL', '$SHARE_URL');|" "$CONFIG_FILE"
|
||||
if [ -f "${CONFIG_FILE}" ]; then
|
||||
echo "🔄 Updating config.php from env vars..."
|
||||
[ -n "${TIMEZONE:-}" ] && sed -i "s|define('TIMEZONE',[[:space:]]*'[^']*');|define('TIMEZONE', '${TIMEZONE}');|" "${CONFIG_FILE}"
|
||||
[ -n "${DATE_TIME_FORMAT:-}" ] && sed -i "s|define('DATE_TIME_FORMAT',[[:space:]]*'[^']*');|define('DATE_TIME_FORMAT', '${DATE_TIME_FORMAT}');|" "${CONFIG_FILE}"
|
||||
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||
sed -i "s|define('TOTAL_UPLOAD_SIZE',[[:space:]]*'[^']*');|define('TOTAL_UPLOAD_SIZE', '${TOTAL_UPLOAD_SIZE}');|" "${CONFIG_FILE}"
|
||||
fi
|
||||
[ -n "${SECURE:-}" ] && sed -i "s|\$envSecure = getenv('SECURE');|\$envSecure = '${SECURE}';|" "${CONFIG_FILE}"
|
||||
[ -n "${SHARE_URL:-}" ] && sed -i "s|define('SHARE_URL',[[:space:]]*'[^']*');|define('SHARE_URL', '${SHARE_URL}');|" "${CONFIG_FILE}"
|
||||
fi
|
||||
|
||||
# Ensure the PHP configuration directory exists
|
||||
# 2.1) Prepare metadata/log for Apache logs
|
||||
mkdir -p /var/www/metadata/log
|
||||
chown www-data:www-data /var/www/metadata/log
|
||||
chmod 775 /var/www/metadata/log
|
||||
|
||||
mkdir -p /var/www/sessions
|
||||
chown www-data:www-data /var/www/sessions
|
||||
chmod 700 /var/www/sessions
|
||||
|
||||
# 2.2) Prepare other dynamic dirs
|
||||
for d in uploads users metadata; do
|
||||
tgt="/var/www/${d}"
|
||||
mkdir -p "${tgt}"
|
||||
chown www-data:www-data "${tgt}"
|
||||
chmod 775 "${tgt}"
|
||||
done
|
||||
|
||||
# 3) Ensure PHP config dir & set upload limits
|
||||
mkdir -p /etc/php/8.3/apache2/conf.d
|
||||
|
||||
# Update PHP upload limits at runtime if TOTAL_UPLOAD_SIZE is set.
|
||||
if [ -n "$TOTAL_UPLOAD_SIZE" ]; then
|
||||
echo "🔄 Updating PHP upload limits with TOTAL_UPLOAD_SIZE=$TOTAL_UPLOAD_SIZE"
|
||||
echo "upload_max_filesize = $TOTAL_UPLOAD_SIZE" > /etc/php/8.3/apache2/conf.d/99-custom.ini
|
||||
echo "post_max_size = $TOTAL_UPLOAD_SIZE" >> /etc/php/8.3/apache2/conf.d/99-custom.ini
|
||||
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||
echo "🔄 Setting PHP upload limits to ${TOTAL_UPLOAD_SIZE}"
|
||||
cat > /etc/php/8.3/apache2/conf.d/99-custom.ini <<EOF
|
||||
upload_max_filesize = ${TOTAL_UPLOAD_SIZE}
|
||||
post_max_size = ${TOTAL_UPLOAD_SIZE}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Update Apache LimitRequestBody based on TOTAL_UPLOAD_SIZE if set.
|
||||
if [ -n "$TOTAL_UPLOAD_SIZE" ]; then
|
||||
size_str=$(echo "$TOTAL_UPLOAD_SIZE" | tr '[:upper:]' '[:lower:]')
|
||||
factor=1
|
||||
# 4) Adjust Apache LimitRequestBody
|
||||
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||
# convert to bytes
|
||||
size_str=$(echo "${TOTAL_UPLOAD_SIZE}" | tr '[:upper:]' '[:lower:]')
|
||||
case "${size_str: -1}" in
|
||||
g)
|
||||
factor=$((1024*1024*1024))
|
||||
size_num=${size_str%g}
|
||||
;;
|
||||
m)
|
||||
factor=$((1024*1024))
|
||||
size_num=${size_str%m}
|
||||
;;
|
||||
k)
|
||||
factor=1024
|
||||
size_num=${size_str%k}
|
||||
;;
|
||||
*)
|
||||
size_num=$size_str
|
||||
;;
|
||||
g) factor=$((1024*1024*1024)); num=${size_str%g} ;;
|
||||
m) factor=$((1024*1024)); num=${size_str%m} ;;
|
||||
k) factor=1024; num=${size_str%k} ;;
|
||||
*) factor=1; num=${size_str} ;;
|
||||
esac
|
||||
LIMIT_REQUEST_BODY=$((size_num * factor))
|
||||
echo "🔄 Setting Apache LimitRequestBody to $LIMIT_REQUEST_BODY bytes (from TOTAL_UPLOAD_SIZE=$TOTAL_UPLOAD_SIZE)"
|
||||
cat <<EOF > /etc/apache2/conf-enabled/limit_request_body.conf
|
||||
LIMIT_REQUEST_BODY=$(( num * factor ))
|
||||
echo "🔄 Setting Apache LimitRequestBody to ${LIMIT_REQUEST_BODY} bytes"
|
||||
cat > /etc/apache2/conf-enabled/limit_request_body.conf <<EOF
|
||||
<Directory "/var/www/public">
|
||||
LimitRequestBody $LIMIT_REQUEST_BODY
|
||||
LimitRequestBody ${LIMIT_REQUEST_BODY}
|
||||
</Directory>
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Set Apache Timeout (default is 300 seconds)
|
||||
echo "🔄 Setting Apache Timeout to 600 seconds"
|
||||
cat <<EOF > /etc/apache2/conf-enabled/timeout.conf
|
||||
# 5) Configure Apache timeout (600s)
|
||||
cat > /etc/apache2/conf-enabled/timeout.conf <<EOF
|
||||
Timeout 600
|
||||
EOF
|
||||
|
||||
echo "🔥 Final Apache Timeout configuration:"
|
||||
cat /etc/apache2/conf-enabled/timeout.conf
|
||||
|
||||
# Update Apache ports if environment variables are provided
|
||||
if [ -n "$HTTP_PORT" ]; then
|
||||
echo "🔄 Setting Apache HTTP port to $HTTP_PORT"
|
||||
sed -i "s/^Listen 80$/Listen $HTTP_PORT/" /etc/apache2/ports.conf
|
||||
sed -i "s/<VirtualHost \*:80>/<VirtualHost *:$HTTP_PORT>/" /etc/apache2/sites-available/000-default.conf
|
||||
# 6) Override ports if provided
|
||||
if [ -n "${HTTP_PORT:-}" ]; then
|
||||
sed -i "s/^Listen 80$/Listen ${HTTP_PORT}/" /etc/apache2/ports.conf
|
||||
sed -i "s/<VirtualHost \*:80>/<VirtualHost *:${HTTP_PORT}>/" /etc/apache2/sites-available/000-default.conf
|
||||
fi
|
||||
if [ -n "${HTTPS_PORT:-}" ]; then
|
||||
sed -i "s/^Listen 443$/Listen ${HTTPS_PORT}/" /etc/apache2/ports.conf
|
||||
fi
|
||||
|
||||
if [ -n "$HTTPS_PORT" ]; then
|
||||
echo "🔄 Setting Apache HTTPS port to $HTTPS_PORT"
|
||||
sed -i "s/^Listen 443$/Listen $HTTPS_PORT/" /etc/apache2/ports.conf
|
||||
fi
|
||||
|
||||
# Update Apache ServerName if environment variable is provided
|
||||
if [ -n "$SERVER_NAME" ]; then
|
||||
echo "🔄 Setting Apache ServerName to $SERVER_NAME"
|
||||
echo "ServerName $SERVER_NAME" >> /etc/apache2/apache2.conf
|
||||
# 7) Set ServerName
|
||||
if [ -n "${SERVER_NAME:-}" ]; then
|
||||
echo "ServerName ${SERVER_NAME}" >> /etc/apache2/apache2.conf
|
||||
else
|
||||
echo "🔄 Setting Apache ServerName to default: FileRise"
|
||||
echo "ServerName FileRise" >> /etc/apache2/apache2.conf
|
||||
fi
|
||||
|
||||
echo "Final /etc/apache2/ports.conf content:"
|
||||
cat /etc/apache2/ports.conf
|
||||
# 8) Prepare dynamic data directories with least privilege
|
||||
for d in uploads users metadata; do
|
||||
tgt="/var/www/${d}"
|
||||
mkdir -p "${tgt}"
|
||||
chown www-data:www-data "${tgt}"
|
||||
chmod 775 "${tgt}"
|
||||
done
|
||||
|
||||
echo "📁 Web app is served from /var/www/public."
|
||||
|
||||
# Ensure the uploads folder exists in /var/www
|
||||
mkdir -p /var/www/uploads
|
||||
echo "🔑 Fixing permissions for /var/www/uploads..."
|
||||
chown -R ${PUID:-99}:${PGID:-100} /var/www/uploads
|
||||
chmod -R 775 /var/www/uploads
|
||||
|
||||
# Ensure the users folder exists in /var/www
|
||||
mkdir -p /var/www/users
|
||||
echo "🔑 Fixing permissions for /var/www/users..."
|
||||
chown -R ${PUID:-99}:${PGID:-100} /var/www/users
|
||||
chmod -R 775 /var/www/users
|
||||
|
||||
# Ensure the metadata folder exists in /var/www
|
||||
mkdir -p /var/www/metadata
|
||||
echo "🔑 Fixing permissions for /var/www/metadata..."
|
||||
chown -R ${PUID:-99}:${PGID:-100} /var/www/metadata
|
||||
chmod -R 775 /var/www/metadata
|
||||
|
||||
# Create users.txt only if it doesn't already exist (preserving persistent data)
|
||||
# 9) Initialize persistent files if absent
|
||||
if [ ! -f /var/www/users/users.txt ]; then
|
||||
echo "ℹ️ users.txt not found in persistent storage; creating new file..."
|
||||
echo "" > /var/www/users/users.txt
|
||||
chown ${PUID:-99}:${PGID:-100} /var/www/users/users.txt
|
||||
chown www-data:www-data /var/www/users/users.txt
|
||||
chmod 664 /var/www/users/users.txt
|
||||
else
|
||||
echo "ℹ️ users.txt already exists; preserving persistent data."
|
||||
fi
|
||||
|
||||
# Create createdTags.json only if it doesn't already exist (preserving persistent data)
|
||||
if [ ! -f /var/www/metadata/createdTags.json ]; then
|
||||
echo "ℹ️ createdTags.json not found in persistent storage; creating new file..."
|
||||
echo "[]" > /var/www/metadata/createdTags.json
|
||||
chown ${PUID:-99}:${PGID:-100} /var/www/metadata/createdTags.json
|
||||
chown www-data:www-data /var/www/metadata/createdTags.json
|
||||
chmod 664 /var/www/metadata/createdTags.json
|
||||
else
|
||||
echo "ℹ️ createdTags.json already exists; preserving persistent data."
|
||||
fi
|
||||
|
||||
# Optionally, fix permissions for the rest of /var/www
|
||||
echo "🔑 Fixing permissions for /var/www..."
|
||||
find /var/www -type f -exec chmod 664 {} \;
|
||||
find /var/www -type d -exec chmod 775 {} \;
|
||||
chown -R ${PUID:-99}:${PGID:-100} /var/www
|
||||
|
||||
echo "🔥 Final PHP configuration (90-custom.ini):"
|
||||
cat /etc/php/8.3/apache2/conf.d/90-custom.ini
|
||||
|
||||
echo "🔥 Final Apache configuration (limit_request_body.conf):"
|
||||
cat /etc/apache2/conf-enabled/limit_request_body.conf
|
||||
|
||||
echo "🔥 Starting Apache..."
|
||||
exec apachectl -D FOREGROUND
|
||||
exec apachectl -D FOREGROUND
|
||||
@@ -1,7 +0,0 @@
|
||||
<IfModule mod_php7.c>
|
||||
php_flag engine off
|
||||
</IfModule>
|
||||
<IfModule mod_php.c>
|
||||
php_flag engine off
|
||||
</IfModule>
|
||||
Options -Indexes
|
||||
@@ -1,3 +0,0 @@
|
||||
<Files "users.txt">
|
||||
Require all denied
|
||||
</Files>
|
||||
Reference in New Issue
Block a user