Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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/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:
|
paths:
|
||||||
- 'CHANGELOG.md'
|
- 'CHANGELOG.md'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync:
|
sync:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
285
CHANGELOG.md
285
CHANGELOG.md
@@ -1,5 +1,286 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
## Changes 4/21/2025 v1.2.2
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -40,7 +321,7 @@
|
|||||||
Refactored to:
|
Refactored to:
|
||||||
1. Fetch CSRF
|
1. Fetch CSRF
|
||||||
2. POST credentials to `/api/auth/auth.php`
|
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.
|
4. Handle full logins vs. TOTP flows cleanly.
|
||||||
|
|
||||||
- **TOTP handlers update**
|
- **TOTP handlers update**
|
||||||
@@ -986,7 +1267,7 @@ The enhancements extend the existing drag-and-drop functionality by adding a hea
|
|||||||
- Adjusted file preview and icon styling for better alignment.
|
- Adjusted file preview and icon styling for better alignment.
|
||||||
- Centered the header and optimized the layout for a clean, modern appearance.
|
- 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
|
FROM ubuntu:24.04 AS appsource
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends ca-certificates && \
|
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
|
RUN mkdir -p /var/www && rm -f /var/www/html/index.html
|
||||||
|
|
||||||
# **Copy the FileRise source** (where your composer.json lives)
|
|
||||||
COPY . /var/www
|
COPY . /var/www
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
@@ -19,88 +16,123 @@ COPY . /var/www
|
|||||||
#############################
|
#############################
|
||||||
FROM composer:2 AS composer
|
FROM composer:2 AS composer
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# **Copy composer files from the source** and install
|
|
||||||
COPY --from=appsource /var/www/composer.json /var/www/composer.lock ./
|
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
|
# Final Stage – runtime image
|
||||||
#############################
|
#############################
|
||||||
FROM ubuntu:24.04
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
LABEL by=error311
|
LABEL by=error311
|
||||||
|
|
||||||
# Set basic environment variables
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
HOME=/root \
|
HOME=/root \
|
||||||
LC_ALL=C.UTF-8 \
|
LC_ALL=C.UTF-8 LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 TERM=xterm \
|
||||||
LANG=en_US.UTF-8 \
|
UPLOAD_MAX_FILESIZE=5G POST_MAX_SIZE=5G TOTAL_UPLOAD_SIZE=5G \
|
||||||
LANGUAGE=en_US.UTF-8 \
|
PERSISTENT_TOKENS_KEY=default_please_change_this_key \
|
||||||
TERM=xterm \
|
PUID=99 PGID=100
|
||||||
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
|
|
||||||
|
|
||||||
# Install Apache, PHP, and required extensions
|
# Install Apache, PHP, and required extensions
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get upgrade -y && \
|
apt-get upgrade -y && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
apache2 \
|
apache2 php php-json php-curl php-zip php-mbstring php-gd php-xml \
|
||||||
php \
|
ca-certificates curl git openssl && \
|
||||||
php-json \
|
apt-get clean && rm -rf /var/lib/apt/lists/* # slim down image
|
||||||
php-curl \
|
|
||||||
php-zip \
|
|
||||||
php-mbstring \
|
|
||||||
php-gd \
|
|
||||||
php-xml \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
git \
|
|
||||||
openssl && \
|
|
||||||
apt-get clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Fix www-data UID/GID
|
# Remap www-data to the PUID/PGID provided for safe bind mounts
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
if [ "$(id -u www-data)" != "${PUID}" ]; then usermod -u ${PUID} www-data || true; fi; \
|
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 || true; fi; \
|
if [ "$(id -g www-data)" != "${PGID}" ]; then groupmod -g "${PGID}" www-data 2>/dev/null || true; fi; \
|
||||||
usermod -g ${PGID} www-data
|
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 custom-php.ini /etc/php/8.3/apache2/conf.d/99-app-tuning.ini
|
||||||
COPY --from=appsource /var/www /var/www
|
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
|
# Secure permissions: code read-only, only data dirs writable
|
||||||
RUN chown -R www-data:www-data /var/www && chmod -R 775 /var/www
|
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.
|
# Apache site configuration
|
||||||
RUN cd /var/www/public && ln -s ../uploads uploads
|
|
||||||
|
|
||||||
# Configure Apache
|
|
||||||
RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
|
RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
|
# Global settings
|
||||||
|
TraceEnable off
|
||||||
|
KeepAlive On
|
||||||
|
MaxKeepAliveRequests 100
|
||||||
|
KeepAliveTimeout 5
|
||||||
|
Timeout 60
|
||||||
|
|
||||||
ServerAdmin webmaster@localhost
|
ServerAdmin webmaster@localhost
|
||||||
DocumentRoot /var/www/public
|
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">
|
<Directory "/var/www/public">
|
||||||
AllowOverride All
|
AllowOverride All
|
||||||
Require all granted
|
Require all granted
|
||||||
DirectoryIndex index.php index.html
|
DirectoryIndex index.html index.php
|
||||||
</Directory>
|
</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>
|
</VirtualHost>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Enable the rewrite and headers modules
|
# Enable required modules
|
||||||
RUN a2enmod rewrite headers
|
RUN a2enmod rewrite headers proxy proxy_fcgi expires deflate ssl
|
||||||
|
|
||||||
# Expose ports and set up start script
|
|
||||||
EXPOSE 80 443
|
EXPOSE 80 443
|
||||||
COPY start.sh /usr/local/bin/start.sh
|
COPY start.sh /usr/local/bin/start.sh
|
||||||
RUN chmod +x /usr/local/bin/start.sh
|
RUN chmod +x /usr/local/bin/start.sh
|
||||||
|
|||||||
1
LICENSE
1
LICENSE
@@ -1,5 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 SeNS
|
||||||
Copyright (c) 2025 FileRise
|
Copyright (c) 2025 FileRise
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -1,7 +1,7 @@
|
|||||||
# FileRise
|
# FileRise
|
||||||
|
|
||||||
**Elevate your File Management** – A modern, self-hosted web file manager.
|
**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:**
|
**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.
|
- 🗃️ **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.
|
- 📚 **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):
|
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).
|
- **Download Files:** Clone this repo or download the [latest release archive](https://github.com/error311/FileRise/releases).
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
git clone https://github.com/error311/FileRise.git
|
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):
|
- **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
|
## 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
|
```bash
|
||||||
# Linux (GVFS/GIO)
|
# 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)
|
- **[phpseclib/phpseclib](https://github.com/phpseclib/phpseclib)** (v~3.0.7)
|
||||||
- **[robthree/twofactorauth](https://github.com/RobThree/TwoFactorAuth)** (v^3.0)
|
- **[robthree/twofactorauth](https://github.com/RobThree/TwoFactorAuth)** (v^3.0)
|
||||||
- **[endroid/qr-code](https://github.com/endroid/qr-code)** (v^5.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
|
### Client-Side Libraries
|
||||||
|
|
||||||
- **Google Fonts** – [Roboto](https://fonts.google.com/specimen/Roboto) and **Material Icons** ([Google Material Icons](https://fonts.google.com/icons))
|
- **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)
|
- **[Bootstrap](https://getbootstrap.com/)** (v4.5.2)
|
||||||
- **[CodeMirror](https://codemirror.net/)** (v5.65.5) – For code editing functionality.
|
- **[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.
|
- **[DOMPurify](https://github.com/cure53/DOMPurify)** (v2.4.0) – For sanitizing HTML.
|
||||||
- **[Fuse.js](https://fusejs.io/)** (v6.6.2) – For indexed, fuzzy searching.
|
- **[Fuse.js](https://fusejs.io/)** (v6.6.2) – For indexed, fuzzy searching.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- Based on [uploader](https://github.com/sensboston/uploader) by @sensboston.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## 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!
|
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_maxlifetime=1440
|
||||||
session.gc_probability=1
|
session.gc_probability=1
|
||||||
session.gc_divisor=100
|
session.gc_divisor=100
|
||||||
|
session.save_path = "/var/www/sessions"
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
; Error Handling / Logging
|
; 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
|
// public/api/addUser.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.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 = new UserController();
|
||||||
$userController->addUser();
|
$userController->addUser();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/admin/getConfig.php
|
// public/api/admin/getConfig.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new AdminController();
|
||||||
$adminController->getConfig();
|
$adminController->getConfig();
|
||||||
44
public/api/admin/readMetadata.php
Normal file
44
public/api/admin/readMetadata.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
// public/api/admin/readMetadata.php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
|
|
||||||
|
// Simple auth‐check: only admins may read these
|
||||||
|
if (empty($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error'=>'Forbidden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect a ?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)) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode((object)[]); // return empty object
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = file_get_contents($path);
|
||||||
|
$json = json_decode($data, true);
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error'=>'Corrupted JSON']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($json);
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/admin/updateConfig.php
|
// public/api/admin/updateConfig.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new AdminController();
|
||||||
$adminController->updateConfig();
|
$adminController->updateConfig();
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/vendor/autoload.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 = new AuthController();
|
||||||
$authController->auth();
|
$authController->auth();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/auth/checkAuth.php
|
// public/api/auth/checkAuth.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new AuthController();
|
||||||
$authController->checkAuth();
|
$authController->checkAuth();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/auth/login_basic.php
|
// public/api/auth/login_basic.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new AuthController();
|
||||||
$authController->loginBasic();
|
$authController->loginBasic();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/auth/logout.php
|
// public/api/auth/logout.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new AuthController();
|
||||||
$authController->logout();
|
$authController->logout();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/auth/token.php
|
// public/api/auth/token.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new AuthController();
|
||||||
$authController->getToken();
|
$authController->getToken();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/changePassword.php
|
// public/api/changePassword.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.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 = new UserController();
|
||||||
$userController->changePassword();
|
$userController->changePassword();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/copyFiles.php
|
// public/api/file/copyFiles.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->copyFiles();
|
$fileController->copyFiles();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/createShareLink.php
|
// public/api/file/createShareLink.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->createShareLink();
|
$fileController->createShareLink();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/deleteFiles.php
|
// public/api/file/deleteFiles.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->deleteFiles();
|
$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
|
// public/api/file/deleteTrashFiles.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->deleteTrashFiles();
|
$fileController->deleteTrashFiles();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/download.php
|
// public/api/file/download.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->downloadFile();
|
$fileController->downloadFile();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/downloadZip.php
|
// public/api/file/downloadZip.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->downloadZip();
|
$fileController->downloadZip();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/extractZip.php
|
// public/api/file/extractZip.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->extractZip();
|
$fileController->extractZip();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/getFileList.php
|
// public/api/file/getFileList.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->getFileList();
|
$fileController->getFileList();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/getFileTag.php
|
// public/api/file/getFileTag.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->getFileTags();
|
$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
|
// public/api/file/getTrashItems.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->getTrashItems();
|
$fileController->getTrashItems();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/moveFiles.php
|
// public/api/file/moveFiles.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->moveFiles();
|
$fileController->moveFiles();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/renameFile.php
|
// public/api/file/renameFile.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->renameFile();
|
$fileController->renameFile();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/restoreFiles.php
|
// public/api/file/restoreFiles.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->restoreFiles();
|
$fileController->restoreFiles();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/saveFile.php
|
// public/api/file/saveFile.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->saveFile();
|
$fileController->saveFile();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/saveFileTag.php
|
// public/api/file/saveFileTag.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->saveFileTag();
|
$fileController->saveFileTag();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/share.php
|
// public/api/file/share.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FileController();
|
||||||
$fileController->shareFile();
|
$fileController->shareFile();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/createFolder.php
|
// public/api/folder/createFolder.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FolderController();
|
||||||
$folderController->createFolder();
|
$folderController->createFolder();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/createShareFolderLink.php
|
// public/api/folder/createShareFolderLink.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FolderController();
|
||||||
$folderController->createShareFolderLink();
|
$folderController->createShareFolderLink();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/deleteFolder.php
|
// public/api/folder/deleteFolder.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FolderController();
|
||||||
$folderController->deleteFolder();
|
$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
|
// public/api/folder/downloadSharedFile.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FolderController();
|
||||||
$folderController->downloadSharedFile();
|
$folderController->downloadSharedFile();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/getFolderList.php
|
// public/api/folder/getFolderList.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FolderController();
|
||||||
$folderController->getFolderList();
|
$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
|
// public/api/folder/renameFolder.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FolderController();
|
||||||
$folderController->renameFolder();
|
$folderController->renameFolder();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/shareFolder.php
|
// public/api/folder/shareFolder.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FolderController();
|
||||||
$folderController->shareFolder();
|
$folderController->shareFolder();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/uploadToSharedFolder.php
|
// public/api/folder/uploadToSharedFolder.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new FolderController();
|
||||||
$folderController->uploadToSharedFolder();
|
$folderController->uploadToSharedFolder();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/getUserPermissions.php
|
// public/api/getUserPermissions.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.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 = new UserController();
|
||||||
$userController->getUserPermissions();
|
$userController->getUserPermissions();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/getUsers.php
|
// public/api/getUsers.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.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 = new UserController();
|
||||||
$userController->getUsers(); // This will output the JSON response
|
$userController->getUsers(); // This will output the JSON response
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/removeUser.php
|
// public/api/removeUser.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.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 = new UserController();
|
||||||
$userController->removeUser();
|
$userController->removeUser();
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/vendor/autoload.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 = new UserController();
|
||||||
$userController->disableTOTP();
|
$userController->disableTOTP();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/totp_recover.php
|
// public/api/totp_recover.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.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 = new UserController();
|
||||||
$userController->recoverTOTP();
|
$userController->recoverTOTP();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/totp_saveCode.php
|
// public/api/totp_saveCode.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.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 = new UserController();
|
||||||
$userController->saveTOTPRecoveryCode();
|
$userController->saveTOTPRecoveryCode();
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/vendor/autoload.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 = new UserController();
|
||||||
$userController->setupTOTP();
|
$userController->setupTOTP();
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/vendor/autoload.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 = new UserController();
|
||||||
$userController->verifyTOTP();
|
$userController->verifyTOTP();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/updateUserPanel.php
|
// public/api/updateUserPanel.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.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 = new UserController();
|
||||||
$userController->updateUserPanel();
|
$userController->updateUserPanel();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/updateUserPermissions.php
|
// public/api/updateUserPermissions.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.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 = new UserController();
|
||||||
$userController->updateUserPermissions();
|
$userController->updateUserPermissions();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/upload/removeChunks.php
|
// public/api/upload/removeChunks.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new UploadController();
|
||||||
$uploadController->removeChunks();
|
$uploadController->removeChunks();
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
// public/api/upload/upload.php
|
// public/api/upload/upload.php
|
||||||
require_once __DIR__ . '/../../../config/config.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 = new UploadController();
|
||||||
$uploadController->handleUpload();
|
$uploadController->handleUpload();
|
||||||
@@ -80,6 +80,9 @@ body.dark-mode .header-container {
|
|||||||
background-color: #1f1f1f;
|
background-color: #1f1f1f;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
}
|
}
|
||||||
|
#darkModeIcon {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.header-logo {
|
.header-logo {
|
||||||
max-height: 50px;
|
max-height: 50px;
|
||||||
|
|||||||
@@ -5,13 +5,6 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title data-i18n-key="title">FileRise</title>
|
<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/png" href="/assets/logo.png">
|
||||||
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
||||||
<meta name="csrf-token" content="">
|
<meta name="csrf-token" content="">
|
||||||
@@ -20,9 +13,12 @@
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
|
<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" />
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||||
<!-- Bootstrap CSS -->
|
<!-- Bootstrap CSS -->
|
||||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css" />
|
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.css">
|
<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"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"
|
||||||
integrity="sha384-UXbkZAbZYZ/KCAslc6UO4d6UHNKsOxZ/sqROSQaPTZCuEIKhfbhmffQ64uXFOcma"
|
integrity="sha384-UXbkZAbZYZ/KCAslc6UO4d6UHNKsOxZ/sqROSQaPTZCuEIKhfbhmffQ64uXFOcma"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
@@ -41,9 +37,9 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
|
||||||
integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E"
|
integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min.js"
|
||||||
integrity="sha384-zPE55eyESN+FxCWGEnlNxGyAPJud6IZ6TtJmXb56OFRGhxZPN4akj9rjA3gw5Qqa"
|
integrity="sha384-zPE55eyESN+FxCWGEnlNxGyAPJud6IZ6TtJmXb56OFRGhxZPN4akj9rjA3gw5Qqa"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<link rel="stylesheet" href="css/styles.css" />
|
<link rel="stylesheet" href="css/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -78,16 +74,16 @@
|
|||||||
stroke: white;
|
stroke: white;
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
stroke: #1565C0;
|
stroke: #1565C0;
|
||||||
stroke-width: 1.5;
|
stroke-width: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer {
|
.drawer {
|
||||||
fill: #FFFFFF;
|
fill: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle {
|
.handle {
|
||||||
fill: #1565C0;
|
fill: #1565C0;
|
||||||
}
|
}
|
||||||
@@ -159,7 +155,11 @@
|
|||||||
<button id="removeUserBtn" data-i18n-title="remove_user" style="display: none;">
|
<button id="removeUserBtn" data-i18n-title="remove_user" style="display: none;">
|
||||||
<i class="material-icons">person_remove</i>
|
<i class="material-icons">person_remove</i>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
<form id="authForm" method="post">
|
<form id="authForm" method="post">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="loginUsername" data-i18n-key="user">User:</label>
|
<label for="loginUsername" data-i18n-key="user">User:</label>
|
||||||
<input type="text" class="form-control" id="loginUsername" name="username" required />
|
<input type="text" class="form-control" id="loginUsername" name="username" required autofocus />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="loginPassword" data-i18n-key="password">Password:</label>
|
<label for="loginPassword" data-i18n-key="password">Password:</label>
|
||||||
@@ -200,7 +200,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Basic HTTP Login Option -->
|
<!-- Basic HTTP Login Option -->
|
||||||
<div class="text-center mt-3">
|
<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
|
<a href="/api/auth/login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic
|
||||||
|
HTTP
|
||||||
Login</a>
|
Login</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,10 +285,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="shareFolderBtn" class="btn btn-secondary ml-2" data-i18n-title="share_folder">
|
<button id="shareFolderBtn" class="btn btn-secondary ml-2" data-i18n-title="share_folder">
|
||||||
<i class="material-icons">share</i>
|
<i class="material-icons">share</i>
|
||||||
</button>
|
</button>
|
||||||
<button id="deleteFolderBtn" class="btn btn-danger ml-2" data-i18n-title="delete_folder">
|
<button id="deleteFolderBtn" class="btn btn-danger ml-2" data-i18n-title="delete_folder">
|
||||||
<i class="material-icons">delete</i>
|
<i class="material-icons">delete</i>
|
||||||
</button>
|
</button>
|
||||||
@@ -391,36 +392,43 @@
|
|||||||
</div> <!-- end mainColumn -->
|
</div> <!-- end mainColumn -->
|
||||||
</div> <!-- end main-wrapper -->
|
</div> <!-- end main-wrapper -->
|
||||||
|
|
||||||
<!-- Download Progress Modal -->
|
<!-- Download Progress Modal -->
|
||||||
<div id="downloadProgressModal" class="modal" style="display: none;">
|
<div id="downloadProgressModal" class="modal" style="display: none;">
|
||||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||||
<!-- Material icon spinner with a dedicated class -->
|
<h4 id="downloadProgressTitle" data-i18n-key="preparing_download">
|
||||||
<span class="material-icons download-spinner">autorenew</span>
|
Preparing your download...
|
||||||
<p data-i18n-key="preparing_download">Preparing your download...</p>
|
</h4>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Single File Download Modal -->
|
<!-- spinner -->
|
||||||
<div id="downloadFileModal" class="modal" style="display: none;">
|
<span class="material-icons download-spinner">autorenew</span>
|
||||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
|
||||||
<h4 data-i18n-key="download_file">Download File</h4>
|
<!-- these were missing -->
|
||||||
<p data-i18n-key="confirm_or_change_filename">Confirm or change the download file name:</p>
|
<progress id="downloadProgressBar" value="0" max="100" style="width:100%; height:1.5em; display:none;"></progress>
|
||||||
<input type="text" id="downloadFileNameInput" class="form-control" data-i18n-placeholder="filename" placeholder="Filename" />
|
<p>
|
||||||
<div style="margin-top: 15px; text-align: right;">
|
<span id="downloadProgressPercent" style="display:none;">0%</span>
|
||||||
<button id="cancelDownloadFile" class="btn btn-secondary"
|
</p>
|
||||||
onclick="document.getElementById('downloadFileModal').style.display = 'none';"
|
</div>
|
||||||
data-i18n-key="cancel">Cancel</button>
|
</div>
|
||||||
<button id="confirmSingleDownloadButton" class="btn btn-primary"
|
|
||||||
onclick="confirmSingleDownload()"
|
<!-- Single File Download Modal -->
|
||||||
data-i18n-key="download">Download</button>
|
<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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
||||||
<div id="changePasswordModal" class="modal" style="display:none;">
|
<div id="changePasswordModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content" style="max-width:400px; margin:auto;">
|
<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"
|
||||||
|
style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">×</span>
|
||||||
<h3 data-i18n-key="change_password_title">Change Password</h3>
|
<h3 data-i18n-key="change_password_title">Change Password</h3>
|
||||||
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
|
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
|
||||||
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
|
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
|
||||||
@@ -434,18 +442,30 @@
|
|||||||
<div id="addUserModal" class="modal">
|
<div id="addUserModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h3 data-i18n-key="create_new_user_title">Create New User</h3>
|
<h3 data-i18n-key="create_new_user_title">Create New User</h3>
|
||||||
<label for="newUsername" data-i18n-key="username">Username:</label>
|
<!-- 1) Add a form around these fields -->
|
||||||
<input type="text" id="newUsername" class="form-control" />
|
<form id="addUserForm">
|
||||||
<label for="addUserPassword" data-i18n-key="password">Password:</label>
|
<label for="newUsername" data-i18n-key="username">Username:</label>
|
||||||
<input type="password" id="addUserPassword" class="form-control" />
|
<input type="text" id="newUsername" class="form-control" required />
|
||||||
<div id="adminCheckboxContainer">
|
|
||||||
<input type="checkbox" id="isAdmin" />
|
<label for="addUserPassword" data-i18n-key="password">Password:</label>
|
||||||
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
|
<input type="password" id="addUserPassword" class="form-control" required />
|
||||||
</div>
|
|
||||||
<div class="button-container">
|
<div id="adminCheckboxContainer">
|
||||||
<button id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
|
<input type="checkbox" id="isAdmin" />
|
||||||
<button id="saveUserBtn" class="btn btn-primary" data-i18n-key="save_user">Save User</button>
|
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div id="removeUserModal" class="modal">
|
<div id="removeUserModal" class="modal">
|
||||||
|
|||||||
658
public/js/adminPanel.js
Normal file
658
public/js/adminPanel.js
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
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.0";
|
||||||
|
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 to fetch a metadata file or return {} on any error
|
||||||
|
const fetchMeta = file =>
|
||||||
|
fetch(`/api/admin/readMetadata.php?file=${file}`, { credentials: "include" })
|
||||||
|
.then(r => r.ok ? r.json() : {}) // non-2xx → treat as empty
|
||||||
|
.catch(() => ({}));
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
fetchMeta("share_folder_links.json"),
|
||||||
|
fetchMeta("share_links.json")
|
||||||
|
])
|
||||||
|
.then(([folders, files]) => {
|
||||||
|
// If nothing at all, show a friendly message
|
||||||
|
if (Object.keys(folders).length === 0 && Object.keys(files).length === 0) {
|
||||||
|
container.textContent = t("no_shared_links_available");
|
||||||
|
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")}:</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" 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>";
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -15,10 +15,9 @@ import {
|
|||||||
openUserPanel,
|
openUserPanel,
|
||||||
openTOTPModal,
|
openTOTPModal,
|
||||||
closeTOTPModal,
|
closeTOTPModal,
|
||||||
openAdminPanel,
|
|
||||||
closeAdminPanel,
|
|
||||||
setLastLoginData
|
setLastLoginData
|
||||||
} from './authModals.js';
|
} from './authModals.js';
|
||||||
|
import { openAdminPanel } from './adminPanel.js';
|
||||||
|
|
||||||
// Production OIDC configuration (override via API as needed)
|
// Production OIDC configuration (override via API as needed)
|
||||||
const currentOIDCConfig = {
|
const currentOIDCConfig = {
|
||||||
@@ -44,6 +43,55 @@ function showToast(msgKey) {
|
|||||||
}
|
}
|
||||||
window.showToast = showToast;
|
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
|
// wrap the TOTP modal opener to disable other login buttons only for Basic/OIDC flows
|
||||||
function openTOTPLoginModal() {
|
function openTOTPLoginModal() {
|
||||||
originalOpenTOTPLoginModal();
|
originalOpenTOTPLoginModal();
|
||||||
@@ -76,10 +124,17 @@ function updateItemsPerPageSelect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
|
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
|
||||||
const authForm = document.getElementById("authForm");
|
const authForm = document.getElementById("authForm");
|
||||||
|
if
|
||||||
if (authForm) authForm.style.display = disableFormLogin ? "none" : "block";
|
(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']");
|
const basicAuthLink = document.querySelector("a[href='/api/auth/login_basic.php']");
|
||||||
if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
|
if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
|
||||||
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||||
@@ -138,7 +193,7 @@ function updateAuthenticatedUI(data) {
|
|||||||
toggleVisibility("mainOperations", true);
|
toggleVisibility("mainOperations", true);
|
||||||
toggleVisibility("uploadFileForm", true);
|
toggleVisibility("uploadFileForm", true);
|
||||||
toggleVisibility("fileListContainer", true);
|
toggleVisibility("fileListContainer", true);
|
||||||
attachEnterKeyListener("addUserModal", "saveUserBtn");
|
//attachEnterKeyListener("addUserModal", "saveUserBtn");
|
||||||
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
|
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
|
||||||
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
||||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||||
@@ -228,6 +283,7 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
}
|
}
|
||||||
window.setupMode = false;
|
window.setupMode = false;
|
||||||
if (data.authenticated) {
|
if (data.authenticated) {
|
||||||
|
localStorage.setItem('isAdmin', data.isAdmin ? 'true' : 'false');
|
||||||
localStorage.setItem("folderOnly", data.folderOnly);
|
localStorage.setItem("folderOnly", data.folderOnly);
|
||||||
localStorage.setItem("readOnly", data.readOnly);
|
localStorage.setItem("readOnly", data.readOnly);
|
||||||
localStorage.setItem("disableUpload", data.disableUpload);
|
localStorage.setItem("disableUpload", data.disableUpload);
|
||||||
@@ -235,6 +291,10 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
if (typeof data.totp_enabled !== "undefined") {
|
if (typeof data.totp_enabled !== "undefined") {
|
||||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
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);
|
updateAuthenticatedUI(data);
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
@@ -276,11 +336,11 @@ async function submitLogin(data) {
|
|||||||
try {
|
try {
|
||||||
const perm = await sendRequest("/api/getUserPermissions.php", "GET");
|
const perm = await sendRequest("/api/getUserPermissions.php", "GET");
|
||||||
if (perm && typeof perm === "object") {
|
if (perm && typeof perm === "object") {
|
||||||
localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false");
|
localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false");
|
||||||
localStorage.setItem("readOnly", perm.readOnly ? "true" : "false");
|
localStorage.setItem("readOnly", perm.readOnly ? "true" : "false");
|
||||||
localStorage.setItem("disableUpload",perm.disableUpload? "true" : "false");
|
localStorage.setItem("disableUpload", perm.disableUpload ? "true" : "false");
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
return window.location.reload();
|
return window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,46 +443,52 @@ function initAuth() {
|
|||||||
submitLogin(formData);
|
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 () {
|
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||||
resetUserForm();
|
resetUserForm();
|
||||||
toggleVisibility("addUserModal", true);
|
toggleVisibility("addUserModal", true);
|
||||||
document.getElementById("newUsername").focus();
|
document.getElementById("newUsername").focus();
|
||||||
});
|
});
|
||||||
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
|
||||||
const newUsername = document.getElementById("newUsername").value.trim();
|
// remove your old saveUserBtn click-handler…
|
||||||
const newPassword = document.getElementById("addUserPassword").value.trim();
|
|
||||||
const isAdmin = document.getElementById("isAdmin").checked;
|
// instead:
|
||||||
if (!newUsername || !newPassword) {
|
const addUserForm = document.getElementById("addUserForm");
|
||||||
showToast("Username and password are required!");
|
addUserForm.addEventListener("submit", function (e) {
|
||||||
return;
|
e.preventDefault(); // stop the browser from reloading the page
|
||||||
}
|
|
||||||
let url = "/api/addUser.php";
|
const newUsername = document.getElementById("newUsername").value.trim();
|
||||||
if (window.setupMode) url += "?setup=1";
|
const newPassword = document.getElementById("addUserPassword").value.trim();
|
||||||
fetch(url, {
|
const isAdmin = document.getElementById("isAdmin").checked;
|
||||||
method: "POST",
|
|
||||||
credentials: "include",
|
if (!newUsername || !newPassword) {
|
||||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
showToast("Username and password are required!");
|
||||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = "/api/addUser.php";
|
||||||
|
if (window.setupMode) url += "?setup=1";
|
||||||
|
|
||||||
|
fetchWithCsrf(url, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast("User added successfully!");
|
||||||
|
closeAddUserModal();
|
||||||
|
checkAuthentication(false);
|
||||||
|
} else {
|
||||||
|
showToast("Error: " + (data.error || "Could not add user"));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.catch(() => {
|
||||||
.then(data => {
|
showToast("Error: Could not add user");
|
||||||
if (data.success) {
|
});
|
||||||
showToast("User added successfully!");
|
});
|
||||||
closeAddUserModal();
|
|
||||||
checkAuthentication(false);
|
|
||||||
} else {
|
|
||||||
showToast("Error: " + (data.error || "Could not add user"));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => { });
|
|
||||||
});
|
|
||||||
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
|
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
|
||||||
|
|
||||||
document.getElementById("removeUserBtn").addEventListener("click", function () {
|
document.getElementById("removeUserBtn").addEventListener("click", function () {
|
||||||
@@ -438,10 +504,10 @@ function initAuth() {
|
|||||||
}
|
}
|
||||||
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
fetch("/api/removeUser.php", {
|
fetchWithCsrf("/api/removeUser.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ username: usernameToRemove })
|
body: JSON.stringify({ username: usernameToRemove })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@@ -477,10 +543,10 @@ function initAuth() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = { oldPassword, newPassword, confirmPassword };
|
const data = { oldPassword, newPassword, confirmPassword };
|
||||||
fetch("/api/changePassword.php", {
|
fetchWithCsrf("/api/changePassword.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { sendRequest } from './networkUtils.js';
|
|||||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||||
import { loadAdminConfigFunc } from './auth.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;
|
let lastLoginData = null;
|
||||||
export function setLastLoginData(data) {
|
export function setLastLoginData(data) {
|
||||||
@@ -230,14 +228,39 @@ export function openUserPanel() {
|
|||||||
|
|
||||||
<!-- New API Docs link -->
|
<!-- New API Docs link -->
|
||||||
<div style="margin-bottom: 15px;">
|
<div style="margin-bottom: 15px;">
|
||||||
<a href="api.html" target="_blank" class="btn btn-secondary">
|
<button type="button" id="openApiModalBtn" class="btn btn-secondary">
|
||||||
${t("api_docs") || "API Docs"}
|
${t("api_docs") || "API Docs"}
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(userPanelModal);
|
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…
|
// Handlers…
|
||||||
document.getElementById("closeUserPanel").addEventListener("click", () => {
|
document.getElementById("closeUserPanel").addEventListener("click", () => {
|
||||||
userPanelModal.style.display = "none";
|
userPanelModal.style.display = "none";
|
||||||
@@ -246,6 +269,7 @@ export function openUserPanel() {
|
|||||||
document.getElementById("changePasswordModal").style.display = "block";
|
document.getElementById("changePasswordModal").style.display = "block";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// TOTP checkbox
|
// TOTP checkbox
|
||||||
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
||||||
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
|
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
|
||||||
@@ -518,495 +542,4 @@ export function closeTOTPModal(disable = true) {
|
|||||||
})
|
})
|
||||||
.catch(() => { showToast(t("error_disabling_totp_setting")); });
|
.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");
|
const checkboxes = document.querySelectorAll(".file-checkbox");
|
||||||
checkboxes.forEach(chk => {
|
checkboxes.forEach(chk => {
|
||||||
chk.checked = masterCheckbox.checked;
|
chk.checked = masterCheckbox.checked;
|
||||||
|
updateRowHighlight(chk);
|
||||||
});
|
});
|
||||||
updateFileActionButtons(); // update buttons based on current selection
|
updateFileActionButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateFileActionButtons() {
|
export function updateFileActionButtons() {
|
||||||
@@ -38,6 +39,21 @@ export function updateFileActionButtons() {
|
|||||||
const zipBtn = document.getElementById("downloadZipBtn");
|
const zipBtn = document.getElementById("downloadZipBtn");
|
||||||
const extractZipBtn = document.getElementById("extractZipBtn");
|
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 (fileCheckboxes.length === 0) {
|
||||||
if (copyBtn) copyBtn.style.display = "none";
|
if (copyBtn) copyBtn.style.display = "none";
|
||||||
if (moveBtn) moveBtn.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 }) {
|
export function buildSearchAndPaginationControls({ currentPage, totalPages, searchTerm }) {
|
||||||
const safeSearchTerm = escapeHTML(searchTerm);
|
const safeSearchTerm = escapeHTML(searchTerm);
|
||||||
// Choose the placeholder text based on advanced search mode
|
// Choose the placeholder text based on advanced search mode
|
||||||
const placeholderText = window.advancedSearchEnabled
|
const placeholderText = window.advancedSearchEnabled
|
||||||
? t("search_placeholder_advanced")
|
? t("search_placeholder_advanced")
|
||||||
: t("search_placeholder");
|
: t("search_placeholder");
|
||||||
|
|
||||||
@@ -101,7 +117,7 @@ export function buildSearchAndPaginationControls({ currentPage, totalPages, sear
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<!-- Advanced Search Toggle Button -->
|
<!-- Advanced Search Toggle Button -->
|
||||||
<div class="input-group-prepend">
|
<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>
|
<i class="material-icons">${window.advancedSearchEnabled ? "filter_alt_off" : "filter_alt"}</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,9 +133,9 @@ export function buildSearchAndPaginationControls({ currentPage, totalPages, sear
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4 text-left">
|
<div class="col-12 col-md-4 text-left">
|
||||||
<div class="d-flex justify-content-center justify-content-md-start align-items-center">
|
<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>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +147,7 @@ export function buildFileTableHeader(sortOrder) {
|
|||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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="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="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>
|
<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)) {
|
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||||
previewIcon = `<i class="material-icons">audiotrack</i>`;
|
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}
|
${previewIcon}
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr onclick="toggleRowSelection(event, '${safeFileName}')" class="clickable-row">
|
<tr class="clickable-row">
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" class="file-checkbox" value="${safeFileName}" onclick="event.stopPropagation(); updateRowHighlight(this);">
|
<input type="checkbox" class="file-checkbox" value="${safeFileName}">
|
||||||
</td>
|
</td>
|
||||||
<td class="file-name-cell">${safeFileName}</td>
|
<td class="file-name-cell">${safeFileName}</td>
|
||||||
<td class="hide-small nowrap">${safeModified}</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 class="hide-small hide-medium nowrap">${safeUploader}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="button-wrap" style="display: flex; justify-content: left; gap: 5px;">
|
<div class="button-wrap" style="display: flex; justify-content: left; gap: 5px;">
|
||||||
<button type="button" class="btn btn-sm btn-success download-btn"
|
<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')}">
|
||||||
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
|
|
||||||
title="${t('download')}">
|
|
||||||
<i class="material-icons">file_download</i>
|
<i class="material-icons">file_download</i>
|
||||||
</button>
|
</button>
|
||||||
${file.editable ? `
|
${file.editable ? `
|
||||||
<button class="btn btn-sm edit-btn"
|
<button class="btn btn-sm edit-btn" data-edit-name="${file.name}" data-edit-folder="${file.folder || 'root'}" title="${t('edit')}">
|
||||||
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
|
||||||
title="${t('edit')}">
|
|
||||||
<i class="material-icons">edit</i>
|
<i class="material-icons">edit</i>
|
||||||
</button>
|
</button>
|
||||||
` : ""}
|
` : ""}
|
||||||
${previewButton}
|
${previewButton}
|
||||||
<button class="btn btn-sm btn-warning rename-btn"
|
<button class="btn btn-sm btn-warning rename-btn" data-rename-name="${file.name}" data-rename-folder="${file.folder || 'root'}" title="${t('rename')}">
|
||||||
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
|
||||||
title="${t('rename')}">
|
|
||||||
<i class="material-icons">drive_file_rename_outline</i>
|
<i class="material-icons">drive_file_rename_outline</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,10 +217,10 @@ export function buildBottomControls(itemsPerPageSetting) {
|
|||||||
return `
|
return `
|
||||||
<div class="d-flex align-items-center mt-3 bottom-controls">
|
<div class="d-flex align-items-center mt-3 bottom-controls">
|
||||||
<label class="label-inline mr-2 mb-0">${t("show")}</label>
|
<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]
|
${[10, 20, 50, 100]
|
||||||
.map(num => `<option value="${num}" ${num === itemsPerPageSetting ? "selected" : ""}>${num}</option>`)
|
.map(num => `<option value="${num}" ${num === itemsPerPageSetting ? "selected" : ""}>${num}</option>`)
|
||||||
.join("")}
|
.join("")}
|
||||||
</select>
|
</select>
|
||||||
<span class="items-per-page-text ml-2 mb-0">${t("items_per_page")}</span>
|
<span class="items-per-page-text ml-2 mb-0">${t("items_per_page")}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,8 +287,6 @@ export function toggleRowSelection(event, fileName) {
|
|||||||
const start = Math.min(currentIndex, lastIndex);
|
const start = Math.min(currentIndex, lastIndex);
|
||||||
const end = Math.max(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++) {
|
for (let i = start; i <= end; i++) {
|
||||||
const cb = allRows[i].querySelector(".file-checkbox");
|
const cb = allRows[i].querySelector(".file-checkbox");
|
||||||
if (cb) {
|
if (cb) {
|
||||||
@@ -345,4 +353,7 @@ export function showCustomConfirmModal(message) {
|
|||||||
yesBtn.addEventListener("click", onYes);
|
yesBtn.addEventListener("click", onYes);
|
||||||
noBtn.addEventListener("click", onNo);
|
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.
|
// Store file details globally for the download confirmation function.
|
||||||
window.singleFileToDownload = fileName;
|
window.singleFileToDownload = fileName;
|
||||||
window.currentFolder = folder || "root";
|
window.currentFolder = folder || "root";
|
||||||
|
|
||||||
// Optionally pre-fill the file name input in the modal.
|
// Optionally pre-fill the file name input in the modal.
|
||||||
const input = document.getElementById("downloadFileNameInput");
|
const input = document.getElementById("downloadFileNameInput");
|
||||||
if (input) {
|
if (input) {
|
||||||
input.value = fileName; // Use file name as-is (or modify if desired)
|
input.value = fileName; // Use file name as-is (or modify if desired)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the single file download modal (a new modal element).
|
// Show the single file download modal (a new modal element).
|
||||||
document.getElementById("downloadFileModal").style.display = "block";
|
document.getElementById("downloadFileModal").style.display = "block";
|
||||||
|
|
||||||
// Optionally focus the input after a short delay.
|
// Optionally focus the input after a short delay.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (input) input.focus();
|
if (input) input.focus();
|
||||||
@@ -97,58 +97,34 @@ export function openDownloadModal(fileName, folder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function confirmSingleDownload() {
|
export function confirmSingleDownload() {
|
||||||
// Get the file name from the modal. Users can change it if desired.
|
// 1) Get and validate the filename
|
||||||
let fileName = document.getElementById("downloadFileNameInput").value.trim();
|
const input = document.getElementById("downloadFileNameInput");
|
||||||
|
const fileName = input.value.trim();
|
||||||
if (!fileName) {
|
if (!fileName) {
|
||||||
showToast("Please enter a name for the file.");
|
showToast("Please enter a name for the file.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide the download modal.
|
// 2) Hide the download-name modal
|
||||||
document.getElementById("downloadFileModal").style.display = "none";
|
document.getElementById("downloadFileModal").style.display = "none";
|
||||||
// Show the progress modal (same as in your ZIP download flow).
|
|
||||||
document.getElementById("downloadProgressModal").style.display = "block";
|
// 3) Build the direct download URL
|
||||||
|
|
||||||
// Build the URL for download.php using GET parameters.
|
|
||||||
const folder = window.currentFolder || "root";
|
const folder = window.currentFolder || "root";
|
||||||
const downloadURL = "/api/file/download.php?folder=" + encodeURIComponent(folder) +
|
const downloadURL = "/api/file/download.php"
|
||||||
"&file=" + encodeURIComponent(window.singleFileToDownload);
|
+ "?folder=" + encodeURIComponent(folder)
|
||||||
|
+ "&file=" + encodeURIComponent(window.singleFileToDownload);
|
||||||
fetch(downloadURL, {
|
|
||||||
method: "GET",
|
// 4) Trigger native browser download
|
||||||
credentials: "include"
|
const a = document.createElement("a");
|
||||||
})
|
a.href = downloadURL;
|
||||||
.then(response => {
|
a.download = fileName;
|
||||||
if (!response.ok) {
|
a.style.display = "none";
|
||||||
return response.text().then(text => {
|
document.body.appendChild(a);
|
||||||
throw new Error("Failed to download file: " + text);
|
a.click();
|
||||||
});
|
document.body.removeChild(a);
|
||||||
}
|
|
||||||
return response.blob();
|
// 5) Notify the user
|
||||||
})
|
showToast("Download started. Check your browser’s download manager.");
|
||||||
.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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleExtractZipSelected(e) {
|
export function handleExtractZipSelected(e) {
|
||||||
@@ -168,16 +144,21 @@ export function handleExtractZipSelected(e) {
|
|||||||
showToast("No zip files selected.");
|
showToast("No zip files selected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change progress modal text to "Extracting files..."
|
// Prepare and show the spinner-only modal
|
||||||
const progressText = document.querySelector("#downloadProgressModal p");
|
const modal = document.getElementById("downloadProgressModal");
|
||||||
if (progressText) {
|
const titleEl = document.getElementById("downloadProgressTitle");
|
||||||
progressText.textContent = "Extracting files...";
|
const spinner = modal.querySelector(".download-spinner");
|
||||||
}
|
const progressBar = document.getElementById("downloadProgressBar");
|
||||||
|
const progressPct = document.getElementById("downloadProgressPercent");
|
||||||
// Show the progress modal.
|
|
||||||
document.getElementById("downloadProgressModal").style.display = "block";
|
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", {
|
fetch("/api/file/extractZip.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@@ -192,45 +173,42 @@ export function handleExtractZipSelected(e) {
|
|||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// Hide the progress modal once the request has completed.
|
modal.style.display = "none";
|
||||||
document.getElementById("downloadProgressModal").style.display = "none";
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
let toastMessage = "Zip file(s) extracted successfully!";
|
let msg = "Zip file(s) extracted successfully!";
|
||||||
if (data.extractedFiles && Array.isArray(data.extractedFiles) && data.extractedFiles.length) {
|
if (Array.isArray(data.extractedFiles) && data.extractedFiles.length) {
|
||||||
toastMessage = "Extracted: " + data.extractedFiles.join(", ");
|
msg = "Extracted: " + data.extractedFiles.join(", ");
|
||||||
}
|
}
|
||||||
showToast(toastMessage);
|
showToast(msg);
|
||||||
loadFileList(window.currentFolder);
|
loadFileList(window.currentFolder);
|
||||||
} else {
|
} else {
|
||||||
showToast("Error extracting zip: " + (data.error || "Unknown error"));
|
showToast("Error extracting zip: " + (data.error || "Unknown error"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
// Hide the progress modal on error.
|
modal.style.display = "none";
|
||||||
document.getElementById("downloadProgressModal").style.display = "none";
|
|
||||||
console.error("Error extracting zip files:", error);
|
console.error("Error extracting zip files:", error);
|
||||||
showToast("Error extracting zip files.");
|
showToast("Error extracting zip files.");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractZipBtn = document.getElementById("extractZipBtn");
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
if (extractZipBtn) {
|
const zipNameModal = document.getElementById("downloadZipModal");
|
||||||
extractZipBtn.replaceWith(extractZipBtn.cloneNode(true));
|
const progressModal = document.getElementById("downloadProgressModal");
|
||||||
document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected);
|
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
||||||
}
|
const confirmZipBtn = document.getElementById("confirmDownloadZip");
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
// 1) Cancel button hides the name modal
|
||||||
const cancelDownloadZip = document.getElementById("cancelDownloadZip");
|
if (cancelZipBtn) {
|
||||||
if (cancelDownloadZip) {
|
cancelZipBtn.addEventListener("click", () => {
|
||||||
cancelDownloadZip.addEventListener("click", function () {
|
zipNameModal.style.display = "none";
|
||||||
document.getElementById("downloadZipModal").style.display = "none";
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This part remains in your confirmDownloadZip event handler:
|
// 2) Confirm button kicks off the zip+download
|
||||||
const confirmDownloadZip = document.getElementById("confirmDownloadZip");
|
if (confirmZipBtn) {
|
||||||
if (confirmDownloadZip) {
|
confirmZipBtn.addEventListener("click", async () => {
|
||||||
confirmDownloadZip.addEventListener("click", function () {
|
// a) Validate ZIP filename
|
||||||
let zipName = document.getElementById("zipFileNameInput").value.trim();
|
let zipName = document.getElementById("zipFileNameInput").value.trim();
|
||||||
if (!zipName) {
|
if (!zipName) {
|
||||||
showToast("Please enter a name for the zip file.");
|
showToast("Please enter a name for the zip file.");
|
||||||
@@ -239,52 +217,56 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
if (!zipName.toLowerCase().endsWith(".zip")) {
|
if (!zipName.toLowerCase().endsWith(".zip")) {
|
||||||
zipName += ".zip";
|
zipName += ".zip";
|
||||||
}
|
}
|
||||||
// Hide the ZIP name input modal
|
|
||||||
document.getElementById("downloadZipModal").style.display = "none";
|
// b) Hide the name‐input modal, show the spinner modal
|
||||||
// Show the progress modal here only on confirm
|
zipNameModal.style.display = "none";
|
||||||
console.log("Download confirmed. Showing progress modal.");
|
progressModal.style.display = "block";
|
||||||
document.getElementById("downloadProgressModal").style.display = "block";
|
|
||||||
const folder = window.currentFolder || "root";
|
// c) (Optional) update the “Preparing…” text if you gave it an ID
|
||||||
fetch("/api/file/downloadZip.php", {
|
const titleEl = document.getElementById("downloadProgressTitle");
|
||||||
method: "POST",
|
if (titleEl) titleEl.textContent = `Preparing ${zipName}…`;
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
try {
|
||||||
"Content-Type": "application/json",
|
// d) POST and await the ZIP blob
|
||||||
"X-CSRF-Token": window.csrfToken
|
const res = await fetch("/api/file/downloadZip.php", {
|
||||||
},
|
method: "POST",
|
||||||
body: JSON.stringify({ folder: folder, files: window.filesToDownload })
|
credentials: "include",
|
||||||
})
|
headers: {
|
||||||
.then(response => {
|
"Content-Type": "application/json",
|
||||||
if (!response.ok) {
|
"X-CSRF-Token": window.csrfToken
|
||||||
return response.text().then(text => {
|
},
|
||||||
throw new Error("Failed to create zip file: " + text);
|
body: JSON.stringify({
|
||||||
});
|
folder: window.currentFolder || "root",
|
||||||
}
|
files: window.filesToDownload
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
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;
|
window.renameFile = renameFile;
|
||||||
@@ -340,6 +340,88 @@ export function renderFileTable(folder, container) {
|
|||||||
|
|
||||||
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
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();
|
createViewToggleButton();
|
||||||
|
|
||||||
// Setup event listeners.
|
// Setup event listeners.
|
||||||
@@ -476,23 +558,26 @@ export function renderGalleryView(folder, container) {
|
|||||||
|
|
||||||
pageFiles.forEach((file, idx) => {
|
pageFiles.forEach((file, idx) => {
|
||||||
const idSafe = encodeURIComponent(file.name) + "-" + (startIdx + idx);
|
const idSafe = encodeURIComponent(file.name) + "-" + (startIdx + idx);
|
||||||
|
const cacheKey = folderPath + encodeURIComponent(file.name);
|
||||||
|
|
||||||
// thumbnail
|
// thumbnail
|
||||||
let thumbnail;
|
let thumbnail;
|
||||||
if (/\.(jpe?g|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
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]) {
|
if (window.imageCache && window.imageCache[cacheKey]) {
|
||||||
thumbnail = `<img src="${window.imageCache[cacheKey]}"
|
thumbnail = `<img
|
||||||
class="gallery-thumbnail"
|
src="${window.imageCache[cacheKey]}"
|
||||||
alt="${escapeHTML(file.name)}"
|
class="gallery-thumbnail"
|
||||||
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
data-cache-key="${cacheKey}"
|
||||||
|
alt="${escapeHTML(file.name)}"
|
||||||
|
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||||||
} else {
|
} else {
|
||||||
const imageUrl = folderPath + encodeURIComponent(file.name) + "?t=" + Date.now();
|
const imageUrl = folderPath + encodeURIComponent(file.name) + "?t=" + Date.now();
|
||||||
thumbnail = `<img src="${imageUrl}"
|
thumbnail = `<img
|
||||||
onload="cacheImage(this,'${cacheKey}')"
|
src="${imageUrl}"
|
||||||
class="gallery-thumbnail"
|
class="gallery-thumbnail"
|
||||||
alt="${escapeHTML(file.name)}"
|
data-cache-key="${cacheKey}"
|
||||||
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
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)) {
|
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||||
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
|
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
|
||||||
@@ -529,9 +614,9 @@ export function renderGalleryView(folder, container) {
|
|||||||
<label for="cb-${idSafe}"
|
<label for="cb-${idSafe}"
|
||||||
style="position:absolute; top:5px; left:5px; width:16px; height:16px;"></label>
|
style="position:absolute; top:5px; left:5px; width:16px; height:16px;"></label>
|
||||||
|
|
||||||
<div class="gallery-preview"
|
<div class="gallery-preview" style="cursor:pointer;"
|
||||||
style="cursor:pointer;"
|
data-preview-url="${folderPath + encodeURIComponent(file.name)}?t=${Date.now()}"
|
||||||
onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t='+Date.now(), '${file.name}')">
|
data-preview-name="${file.name}">
|
||||||
${thumbnail}
|
${thumbnail}
|
||||||
</div>
|
</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;">
|
<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"
|
<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')}">
|
title="${t('download')}">
|
||||||
<i class="material-icons">file_download</i>
|
<i class="material-icons">file_download</i>
|
||||||
</button>
|
</button>
|
||||||
${file.editable ? `
|
${file.editable ? `
|
||||||
<button class="btn btn-sm edit-btn"
|
<button type="button" class="btn btn-sm edit-btn"
|
||||||
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
data-edit-name="${escapeHTML(file.name)}"
|
||||||
title="${t('Edit')}">
|
data-edit-folder="${file.folder || "root"}"
|
||||||
|
title="${t('edit')}">
|
||||||
<i class="material-icons">edit</i>
|
<i class="material-icons">edit</i>
|
||||||
</button>` : ""}
|
</button>` : ""}
|
||||||
<button class="btn btn-sm btn-warning rename-btn"
|
<button type="button" class="btn btn-sm btn-warning rename-btn"
|
||||||
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
data-rename-name="${escapeHTML(file.name)}"
|
||||||
|
data-rename-folder="${file.folder || "root"}"
|
||||||
title="${t('rename')}">
|
title="${t('rename')}">
|
||||||
<i class="material-icons">drive_file_rename_outline</i>
|
<i class="material-icons">drive_file_rename_outline</i>
|
||||||
</button>
|
</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)}"
|
data-file="${escapeHTML(file.name)}"
|
||||||
title="${t('share')}">
|
title="${t('share')}">
|
||||||
<i class="material-icons">share</i>
|
<i class="material-icons">share</i>
|
||||||
@@ -579,13 +667,93 @@ export function renderGalleryView(folder, container) {
|
|||||||
// render
|
// render
|
||||||
fileListContent.innerHTML = galleryHTML;
|
fileListContent.innerHTML = galleryHTML;
|
||||||
|
|
||||||
// ensure toggle button
|
// --- Now wire up all behaviors without inline handlers ---
|
||||||
createViewToggleButton();
|
|
||||||
|
|
||||||
// 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
|
// checkboxes
|
||||||
document.querySelectorAll(".file-checkbox").forEach(cb => {
|
fileListContent.querySelectorAll(".file-checkbox").forEach(cb => {
|
||||||
cb.addEventListener("change", () => updateFileActionButtons());
|
cb.addEventListener("change", () => updateFileActionButtons());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -603,14 +771,13 @@ export function renderGalleryView(folder, container) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// pagination
|
// pagination functions
|
||||||
window.changePage = newPage => {
|
window.changePage = newPage => {
|
||||||
window.currentPage = newPage;
|
window.currentPage = newPage;
|
||||||
if (window.viewMode === "gallery") renderGalleryView(folder);
|
if (window.viewMode === "gallery") renderGalleryView(folder);
|
||||||
else renderFileTable(folder);
|
else renderFileTable(folder);
|
||||||
};
|
};
|
||||||
|
|
||||||
// items per page
|
|
||||||
window.changeItemsPerPage = cnt => {
|
window.changeItemsPerPage = cnt => {
|
||||||
window.itemsPerPage = +cnt;
|
window.itemsPerPage = +cnt;
|
||||||
localStorage.setItem("itemsPerPage", cnt);
|
localStorage.setItem("itemsPerPage", cnt);
|
||||||
@@ -619,8 +786,9 @@ export function renderGalleryView(folder, container) {
|
|||||||
else renderFileTable(folder);
|
else renderFileTable(folder);
|
||||||
};
|
};
|
||||||
|
|
||||||
// update toolbar buttons
|
// update toolbar and toggle button
|
||||||
updateFileActionButtons();
|
updateFileActionButtons();
|
||||||
|
createViewToggleButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Responsive slider constraints based on screen size.
|
// Responsive slider constraints based on screen size.
|
||||||
|
|||||||
@@ -4,36 +4,68 @@ import { fileData } from './fileListView.js';
|
|||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
|
|
||||||
export function openShareModal(file, folder) {
|
export function openShareModal(file, folder) {
|
||||||
|
// Remove any existing modal
|
||||||
const existing = document.getElementById("shareModal");
|
const existing = document.getElementById("shareModal");
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
// Build the modal
|
||||||
const modal = document.createElement("div");
|
const modal = document.createElement("div");
|
||||||
modal.id = "shareModal";
|
modal.id = "shareModal";
|
||||||
modal.classList.add("modal");
|
modal.classList.add("modal");
|
||||||
modal.innerHTML = `
|
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">
|
<div class="modal-header">
|
||||||
<h3>${t("share_file")}: ${escapeHTML(file.name)}</h3>
|
<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>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>${t("set_expiration")}</p>
|
<p>${t("set_expiration")}</p>
|
||||||
<select id="shareExpiration">
|
<select id="shareExpiration" style="width:100%;padding:5px;">
|
||||||
<option value="30">30 minutes</option>
|
<option value="30">30 ${t("minutes")}</option>
|
||||||
<option value="60" selected>60 minutes</option>
|
<option value="60" selected>60 ${t("minutes")}</option>
|
||||||
<option value="120">120 minutes</option>
|
<option value="120">120 ${t("minutes")}</option>
|
||||||
<option value="180">180 minutes</option>
|
<option value="180">180 ${t("minutes")}</option>
|
||||||
<option value="240">240 minutes</option>
|
<option value="240">240 ${t("minutes")}</option>
|
||||||
<option value="1440">1 Day</option>
|
<option value="1440">1 ${t("day")}</option>
|
||||||
|
<option value="custom">${t("custom")}…</option>
|
||||||
</select>
|
</select>
|
||||||
<p>${t("password_optional")}</p>
|
|
||||||
<input type="text" id="sharePassword" placeholder=${t("password_optional")} style="width: 100%;"/>
|
<div id="customExpirationContainer" style="display:none;margin-top:10px;">
|
||||||
<br>
|
<label for="customExpirationValue">${t("duration")}:</label>
|
||||||
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">${t("generate_share_link")}</button>
|
<input type="number" id="customExpirationValue" min="1" value="1" style="width:60px;margin:0 8px;"/>
|
||||||
<div id="shareLinkDisplay" style="margin-top: 10px; display:none;">
|
<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>
|
<p>${t("shareable_link")}</p>
|
||||||
<input type="text" id="shareLinkInput" readonly style="width:100%;"/>
|
<input type="text" id="shareLinkInput" readonly style="width:100%;padding:5px;"/>
|
||||||
<button id="copyShareLinkBtn" class="btn btn-primary" style="margin-top:5px;">${t("copy_link")}</button>
|
<button id="copyShareLinkBtn" class="btn btn-secondary" style="margin-top:5px;">
|
||||||
|
${t("copy_link")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,52 +73,72 @@ export function openShareModal(file, folder) {
|
|||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
modal.style.display = "block";
|
modal.style.display = "block";
|
||||||
|
|
||||||
document.getElementById("closeShareModal").addEventListener("click", () => {
|
// Close handler
|
||||||
modal.remove();
|
document.getElementById("closeShareModal")
|
||||||
});
|
.addEventListener("click", () => modal.remove());
|
||||||
|
|
||||||
document.getElementById("generateShareLinkBtn").addEventListener("click", () => {
|
// Show/hide custom-duration inputs
|
||||||
const expiration = document.getElementById("shareExpiration").value;
|
document.getElementById("shareExpiration")
|
||||||
const password = document.getElementById("sharePassword").value;
|
.addEventListener("change", e => {
|
||||||
fetch("/api/file/createShareLink.php", {
|
const container = document.getElementById("customExpirationContainer");
|
||||||
method: "POST",
|
container.style.display = e.target.value === "custom" ? "block" : "none";
|
||||||
credentials: "include",
|
});
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
// Generate share link
|
||||||
"X-CSRF-Token": window.csrfToken
|
document.getElementById("generateShareLinkBtn")
|
||||||
},
|
.addEventListener("click", () => {
|
||||||
body: JSON.stringify({
|
const sel = document.getElementById("shareExpiration");
|
||||||
folder: folder,
|
let value, unit;
|
||||||
file: file.name,
|
|
||||||
expirationMinutes: parseInt(expiration),
|
if (sel.value === "custom") {
|
||||||
password: password
|
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(res => res.json())
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.token) {
|
if (data.token) {
|
||||||
const shareEndpoint = `${window.location.origin}/api/file/share.php`;
|
const url = `${window.location.origin}/api/file/share.php?token=${encodeURIComponent(data.token)}`;
|
||||||
const shareUrl = `${shareEndpoint}?token=${encodeURIComponent(data.token)}`;
|
document.getElementById("shareLinkInput").value = url;
|
||||||
const displayDiv = document.getElementById("shareLinkDisplay");
|
document.getElementById("shareLinkDisplay").style.display = "block";
|
||||||
const inputField = document.getElementById("shareLinkInput");
|
|
||||||
inputField.value = shareUrl;
|
|
||||||
displayDiv.style.display = "block";
|
|
||||||
} else {
|
} else {
|
||||||
showToast("Error generating share link: " + (data.error || "Unknown error"));
|
showToast(t("error_generating_share") + ": " + (data.error||"Unknown"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error("Error generating share link:", err);
|
console.error(err);
|
||||||
showToast("Error generating share link.");
|
showToast(t("error_generating_share"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("copyShareLinkBtn").addEventListener("click", () => {
|
// Copy to clipboard
|
||||||
const input = document.getElementById("shareLinkInput");
|
document.getElementById("copyShareLinkBtn")
|
||||||
input.select();
|
.addEventListener("click", () => {
|
||||||
document.execCommand("copy");
|
const input = document.getElementById("shareLinkInput");
|
||||||
showToast("Link copied to clipboard!");
|
input.select();
|
||||||
});
|
document.execCommand("copy");
|
||||||
|
showToast(t("link_copied"));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function previewFile(fileUrl, fileName) {
|
export function previewFile(fileUrl, fileName) {
|
||||||
@@ -364,16 +416,21 @@ export function previewFile(fileUrl, fileName) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle non-image file previews.
|
// Handle non-image file previews.
|
||||||
if (extension === "pdf") {
|
if (extension === "pdf") {
|
||||||
const embed = document.createElement("embed");
|
// build a cache‐busted URL
|
||||||
const separator = fileUrl.indexOf('?') === -1 ? '?' : '&';
|
const separator = fileUrl.includes('?') ? '&' : '?';
|
||||||
embed.src = fileUrl + separator + 't=' + new Date().getTime();
|
const urlWithTs = fileUrl + separator + 't=' + Date.now();
|
||||||
embed.type = "application/pdf";
|
|
||||||
embed.style.width = "80vw";
|
// open in a new tab (avoids CSP frame-ancestors)
|
||||||
embed.style.height = "80vh";
|
window.open(urlWithTs, "_blank");
|
||||||
embed.style.border = "none";
|
|
||||||
container.appendChild(embed);
|
// tear down the just-created modal
|
||||||
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(fileName)) {
|
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");
|
const video = document.createElement("video");
|
||||||
video.src = fileUrl;
|
video.src = fileUrl;
|
||||||
video.controls = true;
|
video.controls = true;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { loadFileList } from './fileListView.js';
|
|||||||
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
|
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
import { openFolderShareModal } from './folderShareModal.js';
|
import { openFolderShareModal } from './folderShareModal.js';
|
||||||
|
import { fetchWithCsrf } from './auth.js';
|
||||||
|
import { loadCsrfToken } from './main.js';
|
||||||
|
|
||||||
/* ----------------------
|
/* ----------------------
|
||||||
Helper Functions (Data/State)
|
Helper Functions (Data/State)
|
||||||
@@ -102,24 +104,26 @@ export function setupBreadcrumbDelegation() {
|
|||||||
|
|
||||||
// Click handler via delegation
|
// Click handler via delegation
|
||||||
function breadcrumbClickHandler(e) {
|
function breadcrumbClickHandler(e) {
|
||||||
|
// find the nearest .breadcrumb-link
|
||||||
const link = e.target.closest(".breadcrumb-link");
|
const link = e.target.closest(".breadcrumb-link");
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const folder = link.getAttribute("data-folder");
|
const folder = link.dataset.folder;
|
||||||
window.currentFolder = folder;
|
window.currentFolder = folder;
|
||||||
localStorage.setItem("lastOpenedFolder", folder);
|
localStorage.setItem("lastOpenedFolder", folder);
|
||||||
|
|
||||||
// Update the container with sanitized breadcrumbs.
|
// rebuild the title safely
|
||||||
const container = document.getElementById("fileListTitle");
|
updateBreadcrumbTitle(folder);
|
||||||
const sanitizedBreadcrumb = DOMPurify.sanitize(renderBreadcrumb(folder));
|
|
||||||
container.innerHTML = t("files_in") + " (" + sanitizedBreadcrumb + ")";
|
|
||||||
|
|
||||||
expandTreePath(folder);
|
expandTreePath(folder);
|
||||||
document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
document.querySelectorAll(".folder-option").forEach(el =>
|
||||||
const targetOption = document.querySelector(`.folder-option[data-folder="${folder}"]`);
|
el.classList.remove("selected")
|
||||||
if (targetOption) targetOption.classList.add("selected");
|
);
|
||||||
|
const target = document.querySelector(`.folder-option[data-folder="${folder}"]`);
|
||||||
|
if (target) target.classList.add("selected");
|
||||||
|
|
||||||
loadFileList(folder);
|
loadFileList(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,11 +337,43 @@ function folderDropHandler(event) {
|
|||||||
/* ----------------------
|
/* ----------------------
|
||||||
Main Folder Tree Rendering and Event Binding
|
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) {
|
export async function loadFolderTree(selectedFolder) {
|
||||||
try {
|
try {
|
||||||
// Check if the user has folder-only permission.
|
// Check if the user has folder-only permission.
|
||||||
await checkUserFolderPermission();
|
await checkUserFolderPermission();
|
||||||
|
|
||||||
// Determine effective root folder.
|
// Determine effective root folder.
|
||||||
const username = localStorage.getItem("username") || "root";
|
const username = localStorage.getItem("username") || "root";
|
||||||
let effectiveRoot = "root";
|
let effectiveRoot = "root";
|
||||||
@@ -351,14 +387,14 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
} else {
|
} else {
|
||||||
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build fetch URL.
|
// Build fetch URL.
|
||||||
let fetchUrl = '/api/folder/getFolderList.php';
|
let fetchUrl = '/api/folder/getFolderList.php';
|
||||||
if (window.userFolderOnly) {
|
if (window.userFolderOnly) {
|
||||||
fetchUrl += '?restricted=1';
|
fetchUrl += '?restricted=1';
|
||||||
}
|
}
|
||||||
console.log("Fetching folder list from:", fetchUrl);
|
console.log("Fetching folder list from:", fetchUrl);
|
||||||
|
|
||||||
// Fetch folder list from the server.
|
// Fetch folder list from the server.
|
||||||
const response = await fetch(fetchUrl);
|
const response = await fetch(fetchUrl);
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
@@ -375,10 +411,10 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
} else if (Array.isArray(folderData)) {
|
} else if (Array.isArray(folderData)) {
|
||||||
folders = folderData;
|
folders = folderData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any global "root" entry.
|
// Remove any global "root" entry.
|
||||||
folders = folders.filter(folder => folder.toLowerCase() !== "root");
|
folders = folders.filter(folder => folder.toLowerCase() !== "root");
|
||||||
|
|
||||||
// If restricted, filter folders: keep only those that start with effectiveRoot + "/" (do not include effectiveRoot itself).
|
// If restricted, filter folders: keep only those that start with effectiveRoot + "/" (do not include effectiveRoot itself).
|
||||||
if (window.userFolderOnly && effectiveRoot !== "root") {
|
if (window.userFolderOnly && effectiveRoot !== "root") {
|
||||||
folders = folders.filter(folder => folder.startsWith(effectiveRoot + "/"));
|
folders = folders.filter(folder => folder.startsWith(effectiveRoot + "/"));
|
||||||
@@ -386,16 +422,16 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
localStorage.setItem("lastOpenedFolder", effectiveRoot);
|
localStorage.setItem("lastOpenedFolder", effectiveRoot);
|
||||||
window.currentFolder = effectiveRoot;
|
window.currentFolder = effectiveRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||||
|
|
||||||
// Render the folder tree.
|
// Render the folder tree.
|
||||||
const container = document.getElementById("folderTreeContainer");
|
const container = document.getElementById("folderTreeContainer");
|
||||||
if (!container) {
|
if (!container) {
|
||||||
console.error("Folder tree container not found.");
|
console.error("Folder tree container not found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = `<div id="rootRow" class="root-row">
|
let html = `<div id="rootRow" class="root-row">
|
||||||
<span class="folder-toggle" data-folder="${effectiveRoot}">[<span class="custom-dash">-</span>]</span>
|
<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>
|
<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");
|
html += renderFolderTree(tree, "", "block");
|
||||||
}
|
}
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
||||||
// Attach drag/drop event listeners.
|
// Attach drag/drop event listeners.
|
||||||
container.querySelectorAll(".folder-option").forEach(el => {
|
container.querySelectorAll(".folder-option").forEach(el => {
|
||||||
el.addEventListener("dragover", folderDragOverHandler);
|
el.addEventListener("dragover", folderDragOverHandler);
|
||||||
el.addEventListener("dragleave", folderDragLeaveHandler);
|
el.addEventListener("dragleave", folderDragLeaveHandler);
|
||||||
el.addEventListener("drop", folderDropHandler);
|
el.addEventListener("drop", folderDropHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (selectedFolder) {
|
if (selectedFolder) {
|
||||||
window.currentFolder = selectedFolder;
|
window.currentFolder = selectedFolder;
|
||||||
}
|
}
|
||||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||||
|
|
||||||
const titleEl = document.getElementById("fileListTitle");
|
// Initial breadcrumb update
|
||||||
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(window.currentFolder) + ")";
|
updateBreadcrumbTitle(window.currentFolder);
|
||||||
setupBreadcrumbDelegation();
|
|
||||||
loadFileList(window.currentFolder);
|
loadFileList(window.currentFolder);
|
||||||
|
|
||||||
const folderState = loadFolderTreeState();
|
const folderState = loadFolderTreeState();
|
||||||
if (window.currentFolder !== effectiveRoot && folderState[window.currentFolder] !== "none") {
|
if (window.currentFolder !== effectiveRoot && folderState[window.currentFolder] !== "none") {
|
||||||
expandTreePath(window.currentFolder);
|
expandTreePath(window.currentFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
||||||
if (selectedEl) {
|
if (selectedEl) {
|
||||||
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||||
selectedEl.classList.add("selected");
|
selectedEl.classList.add("selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Folder-option click: update selection, breadcrumbs, and file list
|
||||||
container.querySelectorAll(".folder-option").forEach(el => {
|
container.querySelectorAll(".folder-option").forEach(el => {
|
||||||
el.addEventListener("click", function (e) {
|
el.addEventListener("click", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -442,13 +478,14 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
const selected = this.getAttribute("data-folder");
|
const selected = this.getAttribute("data-folder");
|
||||||
window.currentFolder = selected;
|
window.currentFolder = selected;
|
||||||
localStorage.setItem("lastOpenedFolder", selected);
|
localStorage.setItem("lastOpenedFolder", selected);
|
||||||
const titleEl = document.getElementById("fileListTitle");
|
|
||||||
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(selected) + ")";
|
// Safe breadcrumb update
|
||||||
setupBreadcrumbDelegation();
|
updateBreadcrumbTitle(selected);
|
||||||
loadFileList(selected);
|
loadFileList(selected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Root toggle handler
|
||||||
const rootToggle = container.querySelector("#rootRow .folder-toggle");
|
const rootToggle = container.querySelector("#rootRow .folder-toggle");
|
||||||
if (rootToggle) {
|
if (rootToggle) {
|
||||||
rootToggle.addEventListener("click", function (e) {
|
rootToggle.addEventListener("click", function (e) {
|
||||||
@@ -471,7 +508,8 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Other folder-toggle handlers
|
||||||
container.querySelectorAll(".folder-toggle").forEach(toggle => {
|
container.querySelectorAll(".folder-toggle").forEach(toggle => {
|
||||||
toggle.addEventListener("click", function (e) {
|
toggle.addEventListener("click", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -494,12 +532,13 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading folder tree:", error);
|
console.error("Error loading folder tree:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// For backward compatibility.
|
// For backward compatibility.
|
||||||
export function loadFolderList(selectedFolder) {
|
export function loadFolderList(selectedFolder) {
|
||||||
loadFolderTree(selectedFolder);
|
loadFolderTree(selectedFolder);
|
||||||
@@ -627,45 +666,53 @@ document.getElementById("cancelCreateFolder").addEventListener("click", function
|
|||||||
document.getElementById("newFolderName").value = "";
|
document.getElementById("newFolderName").value = "";
|
||||||
});
|
});
|
||||||
attachEnterKeyListener("createFolderModal", "submitCreateFolder");
|
attachEnterKeyListener("createFolderModal", "submitCreateFolder");
|
||||||
document.getElementById("submitCreateFolder").addEventListener("click", function () {
|
document.getElementById("submitCreateFolder").addEventListener("click", async () => {
|
||||||
const folderInput = document.getElementById("newFolderName").value.trim();
|
const folderInput = document.getElementById("newFolderName").value.trim();
|
||||||
if (!folderInput) {
|
if (!folderInput) return showToast("Please enter a folder name.");
|
||||||
showToast("Please enter a folder name.");
|
|
||||||
return;
|
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;
|
// 2) Call with fetchWithCsrf
|
||||||
if (selectedFolder && selectedFolder !== "root") {
|
fetchWithCsrf("/api/folder/createFolder.php", {
|
||||||
fullFolderName = selectedFolder + "/" + folderInput;
|
|
||||||
}
|
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
||||||
fetch("/api/folder/createFolder.php", {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
"Content-Type": "application/json",
|
body: JSON.stringify({ folderName: folderInput, parent })
|
||||||
"X-CSRF-Token": csrfToken
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
folderName: folderInput,
|
|
||||||
parent: selectedFolder === "root" ? "" : selectedFolder
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(async res => {
|
||||||
.then(data => {
|
if (!res.ok) {
|
||||||
if (data.success) {
|
// pull out a JSON error, or fallback to status text
|
||||||
showToast("Folder created successfully!");
|
let err;
|
||||||
window.currentFolder = fullFolderName;
|
try {
|
||||||
localStorage.setItem("lastOpenedFolder", fullFolderName);
|
const j = await res.json();
|
||||||
loadFolderList(fullFolderName);
|
err = j.error || j.message || res.statusText;
|
||||||
} else {
|
} catch {
|
||||||
showToast("Error: " + (data.error || "Could not create folder"));
|
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("createFolderModal").style.display = "none";
|
||||||
document.getElementById("newFolderName").value = "";
|
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 { escapeHTML, showToast } from './domUtils.js';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
|
|
||||||
export function openFolderShareModal(folder) {
|
export function openFolderShareModal(folder) {
|
||||||
// Remove any existing folder share modal
|
// Remove any existing modal
|
||||||
const existing = document.getElementById("folderShareModal");
|
const existing = document.getElementById("folderShareModal");
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
|
|
||||||
// Create the modal container
|
// Build modal
|
||||||
const modal = document.createElement("div");
|
const modal = document.createElement("div");
|
||||||
modal.id = "folderShareModal";
|
modal.id = "folderShareModal";
|
||||||
modal.classList.add("modal");
|
modal.classList.add("modal");
|
||||||
modal.innerHTML = `
|
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">
|
<div class="modal-header">
|
||||||
<h3>${t("share_folder")}: ${escapeHTML(folder)}</h3>
|
<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>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>${t("set_expiration")}</p>
|
<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="30">30 ${t("minutes")}</option>
|
||||||
<option value="60" selected>60 ${t("minutes")}</option>
|
<option value="60" selected>60 ${t("minutes")}</option>
|
||||||
<option value="120">120 ${t("minutes")}</option>
|
<option value="120">120 ${t("minutes")}</option>
|
||||||
<option value="180">180 ${t("minutes")}</option>
|
<option value="180">180 ${t("minutes")}</option>
|
||||||
<option value="240">240 ${t("minutes")}</option>
|
<option value="240">240 ${t("minutes")}</option>
|
||||||
<option value="1440">1 ${t("day")}</option>
|
<option value="1440">1 ${t("day")}</option>
|
||||||
|
<option value="custom">${t("custom")}…</option>
|
||||||
</select>
|
</select>
|
||||||
<p>${t("password_optional")}</p>
|
|
||||||
<input type="text" id="folderSharePassword" placeholder="${t("enter_password")}" style="width: 100%;"/>
|
<div id="customFolderExpirationContainer" style="display:none;margin-top:10px;">
|
||||||
<br>
|
<label for="customFolderExpirationValue">${t("duration")}:</label>
|
||||||
<label>
|
<input type="number" id="customFolderExpirationValue" min="1" value="1" style="width:60px;margin:0 8px;"/>
|
||||||
<input type="checkbox" id="folderShareAllowUpload"> ${t("allow_uploads")}
|
<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>
|
</label>
|
||||||
<br><br>
|
|
||||||
<button id="generateFolderShareLinkBtn" class="btn btn-primary" style="margin-top: 10px;">${t("generate_share_link")}</button>
|
<button
|
||||||
<div id="folderShareLinkDisplay" style="margin-top: 10px; display: none;">
|
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>
|
<p>${t("shareable_link")}</p>
|
||||||
<input type="text" id="folderShareLinkInput" readonly style="width: 100%;"/>
|
<input type="text" id="folderShareLinkInput" readonly style="width:100%;padding:5px;"/>
|
||||||
<button id="copyFolderShareLinkBtn" class="btn btn-primary" style="margin-top: 5px;">${t("copy_link")}</button>
|
<button id="copyFolderShareLinkBtn" class="btn btn-secondary" style="margin-top:5px;">
|
||||||
|
${t("copy_link")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,62 +77,75 @@ export function openFolderShareModal(folder) {
|
|||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
modal.style.display = "block";
|
modal.style.display = "block";
|
||||||
|
|
||||||
// Close button handler
|
// Close
|
||||||
document.getElementById("closeFolderShareModal").addEventListener("click", () => {
|
document.getElementById("closeFolderShareModal")
|
||||||
modal.remove();
|
.addEventListener("click", () => modal.remove());
|
||||||
});
|
|
||||||
|
|
||||||
// Handler for generating the share link
|
// Toggle custom inputs
|
||||||
document.getElementById("generateFolderShareLinkBtn").addEventListener("click", () => {
|
document.getElementById("folderShareExpiration")
|
||||||
const expiration = document.getElementById("folderShareExpiration").value;
|
.addEventListener("change", e => {
|
||||||
const password = document.getElementById("folderSharePassword").value;
|
document.getElementById("customFolderExpirationContainer")
|
||||||
const allowUpload = document.getElementById("folderShareAllowUpload").checked ? 1 : 0;
|
.style.display = e.target.value === "custom" ? "block" : "none";
|
||||||
|
});
|
||||||
// Retrieve the CSRF token from the meta tag.
|
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
|
// Generate link
|
||||||
if (!csrfToken) {
|
document.getElementById("generateFolderShareLinkBtn")
|
||||||
showToast(t("csrf_error"));
|
.addEventListener("click", () => {
|
||||||
return;
|
const sel = document.getElementById("folderShareExpiration");
|
||||||
}
|
let value, unit;
|
||||||
// Post to the createFolderShareLink endpoint.
|
if (sel.value === "custom") {
|
||||||
fetch("/api/folder/createShareFolderLink.php", {
|
value = parseInt(document.getElementById("customFolderExpirationValue").value, 10);
|
||||||
method: "POST",
|
unit = document.getElementById("customFolderExpirationUnit").value;
|
||||||
credentials: "include",
|
} else {
|
||||||
headers: {
|
value = parseInt(sel.value, 10);
|
||||||
"Content-Type": "application/json",
|
unit = "minutes";
|
||||||
"X-CSRF-Token": csrfToken
|
}
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
const password = document.getElementById("folderSharePassword").value;
|
||||||
folder: folder,
|
const allowUpload = document.getElementById("folderShareAllowUpload").checked ? 1 : 0;
|
||||||
expirationMinutes: parseInt(expiration, 10),
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
|
||||||
password: password,
|
if (!csrfToken) {
|
||||||
allowUpload: allowUpload
|
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(r => r.json())
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.token && data.link) {
|
if (data.token && data.link) {
|
||||||
const shareUrl = data.link;
|
document.getElementById("folderShareLinkInput").value = data.link;
|
||||||
const displayDiv = document.getElementById("folderShareLinkDisplay");
|
document.getElementById("folderShareLinkDisplay").style.display = "block";
|
||||||
const inputField = document.getElementById("folderShareLinkInput");
|
|
||||||
inputField.value = shareUrl;
|
|
||||||
displayDiv.style.display = "block";
|
|
||||||
showToast(t("share_link_generated"));
|
showToast(t("share_link_generated"));
|
||||||
} else {
|
} 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 => {
|
.catch(err => {
|
||||||
console.error("Error generating folder share link:", err);
|
console.error(err);
|
||||||
showToast(t("error_generating_share_link") + ": " + (err.error || t("unknown_error")));
|
showToast(t("error_generating_share_link") + ": " + t("unknown_error"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy share link button handler
|
// Copy
|
||||||
document.getElementById("copyFolderShareLinkBtn").addEventListener("click", () => {
|
document.getElementById("copyFolderShareLinkBtn")
|
||||||
const input = document.getElementById("folderShareLinkInput");
|
.addEventListener("click", () => {
|
||||||
input.select();
|
const inp = document.getElementById("folderShareLinkInput");
|
||||||
document.execCommand("copy");
|
inp.select();
|
||||||
showToast(t("link_copied"));
|
document.execCommand("copy");
|
||||||
});
|
showToast(t("link_copied"));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -150,6 +150,13 @@ const translations = {
|
|||||||
"allow_uploads": "Allow Uploads",
|
"allow_uploads": "Allow Uploads",
|
||||||
"share_link_generated": "Share Link Generated",
|
"share_link_generated": "Share Link Generated",
|
||||||
"error_generating_share_link": "Error Generating Share Link",
|
"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
|
||||||
"folder_share": "Share Folder",
|
"folder_share": "Share Folder",
|
||||||
@@ -166,16 +173,30 @@ const translations = {
|
|||||||
"user": "User:",
|
"user": "User:",
|
||||||
"unknown_error": "Unknown Error",
|
"unknown_error": "Unknown Error",
|
||||||
"link_copied": "Link Copied to Clipboard",
|
"link_copied": "Link Copied to Clipboard",
|
||||||
"minutes": "minutes",
|
|
||||||
"hours": "hours",
|
|
||||||
"days": "days",
|
|
||||||
"weeks": "weeks",
|
"weeks": "weeks",
|
||||||
"months": "months",
|
"months": "months",
|
||||||
"seconds": "seconds",
|
|
||||||
|
|
||||||
// Dark Mode Toggle
|
// Dark Mode Toggle
|
||||||
"dark_mode_toggle": "Dark Mode",
|
"dark_mode_toggle": "Dark Mode",
|
||||||
"light_mode_toggle": "Light 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:
|
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
|
||||||
"admin_panel": "Admin Panel",
|
"admin_panel": "Admin Panel",
|
||||||
@@ -237,7 +258,7 @@ const translations = {
|
|||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"show": "Show",
|
"show": "Show",
|
||||||
"items_per_page": "items per page",
|
"items_per_page": "items per page",
|
||||||
"columns":"Columns",
|
"columns": "Columns",
|
||||||
"api_docs": "API Docs"
|
"api_docs": "API Docs"
|
||||||
},
|
},
|
||||||
es: {
|
es: {
|
||||||
@@ -804,7 +825,7 @@ const translations = {
|
|||||||
"prev": "Zurück",
|
"prev": "Zurück",
|
||||||
"next": "Weiter",
|
"next": "Weiter",
|
||||||
"page": "Seite",
|
"page": "Seite",
|
||||||
"of": "von",
|
"of": "von",
|
||||||
|
|
||||||
// Login Form keys:
|
// Login Form keys:
|
||||||
"login": "Anmelden",
|
"login": "Anmelden",
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { sendRequest } from './networkUtils.js';
|
import { sendRequest } from './networkUtils.js';
|
||||||
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js';
|
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js';
|
||||||
import { loadFolderTree } from './folderManager.js';
|
|
||||||
import { initUpload } from './upload.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 { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
||||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
||||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
||||||
@@ -12,39 +14,61 @@ import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload }
|
|||||||
import { editFile, saveFile } from './fileEditor.js';
|
import { editFile, saveFile } from './fileEditor.js';
|
||||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||||
|
|
||||||
// Remove the retry logic version and just use loadCsrfToken directly:
|
|
||||||
function loadCsrfToken() {
|
export function loadCsrfToken() {
|
||||||
return fetch('/api/auth/token.php', { credentials: 'include' })
|
return fetchWithCsrf('/api/auth/token.php', {
|
||||||
.then(response => {
|
method: 'GET'
|
||||||
if (!response.ok) {
|
})
|
||||||
throw new Error("Token fetch failed with status: " + response.status);
|
.then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Token fetch failed with status ${res.status}`);
|
||||||
}
|
}
|
||||||
return response.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(({ csrf_token, share_url }) => {
|
||||||
window.csrfToken = data.csrf_token;
|
// Update global and <meta>
|
||||||
window.SHARE_URL = data.share_url;
|
window.csrfToken = csrf_token;
|
||||||
|
let meta = document.querySelector('meta[name="csrf-token"]');
|
||||||
let metaCSRF = document.querySelector('meta[name="csrf-token"]');
|
if (!meta) {
|
||||||
if (!metaCSRF) {
|
meta = document.createElement('meta');
|
||||||
metaCSRF = document.createElement('meta');
|
meta.name = 'csrf-token';
|
||||||
metaCSRF.name = 'csrf-token';
|
document.head.appendChild(meta);
|
||||||
document.head.appendChild(metaCSRF);
|
|
||||||
}
|
}
|
||||||
metaCSRF.setAttribute('content', data.csrf_token);
|
meta.content = csrf_token;
|
||||||
|
|
||||||
let metaShare = document.querySelector('meta[name="share-url"]');
|
let shareMeta = document.querySelector('meta[name="share-url"]');
|
||||||
if (!metaShare) {
|
if (!shareMeta) {
|
||||||
metaShare = document.createElement('meta');
|
shareMeta = document.createElement('meta');
|
||||||
metaShare.name = 'share-url';
|
shareMeta.name = 'share-url';
|
||||||
document.head.appendChild(metaShare);
|
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.
|
// Expose functions for inline handlers.
|
||||||
window.sendRequest = sendRequest;
|
window.sendRequest = sendRequest;
|
||||||
@@ -115,48 +139,55 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
|
|
||||||
// --- Dark Mode Persistence ---
|
// --- Dark Mode Persistence ---
|
||||||
const darkModeToggle = document.getElementById("darkModeToggle");
|
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||||
const storedDarkMode = localStorage.getItem("darkMode");
|
const darkModeIcon = document.getElementById("darkModeIcon");
|
||||||
|
|
||||||
if (storedDarkMode === "true") {
|
if (darkModeToggle && darkModeIcon) {
|
||||||
document.body.classList.add("dark-mode");
|
// 1) Load stored preference (or null)
|
||||||
} else if (storedDarkMode === "false") {
|
let stored = localStorage.getItem("darkMode");
|
||||||
document.body.classList.remove("dark-mode");
|
const hasStored = stored !== null;
|
||||||
} else {
|
|
||||||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
// 2) Determine initial mode
|
||||||
document.body.classList.add("dark-mode");
|
const isDark = hasStored
|
||||||
} else {
|
? (stored === "true")
|
||||||
document.body.classList.remove("dark-mode");
|
: (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) {
|
updateIcon();
|
||||||
darkModeToggle.textContent = document.body.classList.contains("dark-mode")
|
|
||||||
? t("light_mode")
|
|
||||||
: t("dark_mode");
|
|
||||||
|
|
||||||
darkModeToggle.addEventListener("click", function () {
|
// 4) Click handler: always override and store preference
|
||||||
if (document.body.classList.contains("dark-mode")) {
|
darkModeToggle.addEventListener("click", () => {
|
||||||
document.body.classList.remove("dark-mode");
|
const nowDark = document.body.classList.toggle("dark-mode");
|
||||||
localStorage.setItem("darkMode", "false");
|
localStorage.setItem("darkMode", nowDark ? "true" : "false");
|
||||||
darkModeToggle.textContent = t("dark_mode");
|
updateIcon();
|
||||||
} else {
|
|
||||||
document.body.classList.add("dark-mode");
|
|
||||||
localStorage.setItem("darkMode", "true");
|
|
||||||
darkModeToggle.textContent = t("light_mode");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (localStorage.getItem("darkMode") === null && window.matchMedia) {
|
// 5) OS‐level change: only if no stored pref at load
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (event) => {
|
if (!hasStored && window.matchMedia) {
|
||||||
if (event.matches) {
|
window
|
||||||
document.body.classList.add("dark-mode");
|
.matchMedia("(prefers-color-scheme: dark)")
|
||||||
if (darkModeToggle) darkModeToggle.textContent = t("light_mode");
|
.addEventListener("change", e => {
|
||||||
} else {
|
document.body.classList.toggle("dark-mode", e.matches);
|
||||||
document.body.classList.remove("dark-mode");
|
updateIcon();
|
||||||
if (darkModeToggle) darkModeToggle.textContent = t("dark_mode");
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// --- End Dark Mode Persistence ---
|
// --- 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,
|
forceChunkSize: true,
|
||||||
testChunks: false,
|
testChunks: false,
|
||||||
throttleProgressCallbacks: 1,
|
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");
|
const fileInput = document.getElementById("file");
|
||||||
@@ -496,26 +501,40 @@ function initResumableUpload() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
resumableInstance.on("fileSuccess", function(file, message) {
|
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) {
|
if (li && li.progressBar) {
|
||||||
li.progressBar.style.width = "100%";
|
li.progressBar.style.width = "100%";
|
||||||
li.progressBar.innerText = "Done";
|
li.progressBar.innerText = "Done";
|
||||||
// Hide pause/resume and remove buttons for successful files.
|
// remove action buttons
|
||||||
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
|
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
|
||||||
if (pauseResumeBtn) {
|
if (pauseResumeBtn) pauseResumeBtn.style.display = "none";
|
||||||
pauseResumeBtn.style.display = "none";
|
|
||||||
}
|
|
||||||
const removeBtn = li.querySelector(".remove-file-btn");
|
const removeBtn = li.querySelector(".remove-file-btn");
|
||||||
if (removeBtn) {
|
if (removeBtn) removeBtn.style.display = "none";
|
||||||
removeBtn.style.display = "none";
|
setTimeout(() => li.remove(), 5000);
|
||||||
}
|
|
||||||
// Schedule removal of the file entry after 5 seconds.
|
|
||||||
setTimeout(() => {
|
|
||||||
li.remove();
|
|
||||||
window.selectedFiles = window.selectedFiles.filter(f => f.uniqueIdentifier !== file.uniqueIdentifier);
|
|
||||||
updateFileInfoCount();
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFileList(window.currentFolder);
|
loadFileList(window.currentFolder);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -618,8 +637,25 @@ function submitFiles(allFiles) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
jsonResponse = null;
|
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];
|
const li = progressElements[file.uploadIndex];
|
||||||
|
|
||||||
if (xhr.status >= 200 && xhr.status < 300 && (!jsonResponse || !jsonResponse.error)) {
|
if (xhr.status >= 200 && xhr.status < 300 && (!jsonResponse || !jsonResponse.error)) {
|
||||||
|
// real success
|
||||||
if (li) {
|
if (li) {
|
||||||
li.progressBar.style.width = "100%";
|
li.progressBar.style.width = "100%";
|
||||||
li.progressBar.innerText = "Done";
|
li.progressBar.innerText = "Done";
|
||||||
@@ -627,11 +663,14 @@ function submitFiles(allFiles) {
|
|||||||
}
|
}
|
||||||
uploadResults[file.uploadIndex] = true;
|
uploadResults[file.uploadIndex] = true;
|
||||||
} else {
|
} else {
|
||||||
|
// real failure
|
||||||
if (li) {
|
if (li) {
|
||||||
li.progressBar.innerText = "Error";
|
li.progressBar.innerText = "Error";
|
||||||
}
|
}
|
||||||
allSucceeded = false;
|
allSucceeded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Only now count this chunk as finished ───────────────────
|
||||||
finishedCount++;
|
finishedCount++;
|
||||||
if (finishedCount === allFiles.length) {
|
if (finishedCount === allFiles.length) {
|
||||||
refreshFileList(allFiles, uploadResults, progressElements);
|
refreshFileList(allFiles, uploadResults, progressElements);
|
||||||
@@ -665,6 +704,7 @@ function submitFiles(allFiles) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
xhr.open("POST", "/api/upload/upload.php", true);
|
xhr.open("POST", "/api/upload/upload.php", true);
|
||||||
|
xhr.withCredentials = true;
|
||||||
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
// public/webdav.php
|
// public/webdav.php
|
||||||
|
|
||||||
|
// ─── 0) Forward Basic auth into PHP_AUTH_* for every HTTP verb ─────────────
|
||||||
if (
|
if (
|
||||||
empty($_SERVER['PHP_AUTH_USER'])
|
empty($_SERVER['PHP_AUTH_USER'])
|
||||||
&& !empty($_SERVER['HTTP_AUTHORIZATION'])
|
&& !empty($_SERVER['HTTP_AUTHORIZATION'])
|
||||||
@@ -11,46 +12,58 @@ if (
|
|||||||
$_SERVER['PHP_AUTH_PW'] = $p;
|
$_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__ . '/../config/config.php'; // UPLOAD_DIR, META_DIR, DATE_TIME_FORMAT
|
||||||
require_once __DIR__ . '/../vendor/autoload.php'; // Composer & SabreDAV
|
require_once __DIR__ . '/../vendor/autoload.php'; // Composer & SabreDAV
|
||||||
require_once __DIR__ . '/../src/models/AuthModel.php'; // AuthModel::authenticate(), getUserRole(), loadFolderPermission()
|
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';
|
require_once __DIR__ . '/../src/webdav/FileRiseDirectory.php';
|
||||||
use Sabre\DAV\Server;
|
use Sabre\DAV\Server;
|
||||||
use Sabre\DAV\Auth\Backend\BasicCallBack;
|
use Sabre\DAV\Auth\Backend\BasicCallBack;
|
||||||
use Sabre\DAV\Auth\Plugin as AuthPlugin;
|
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\Plugin as LocksPlugin;
|
||||||
use Sabre\DAV\Locks\Backend\File as LocksFileBackend;
|
use Sabre\DAV\Locks\Backend\File as LocksFileBackend;
|
||||||
use FileRise\WebDAV\FileRiseDirectory;
|
use FileRise\WebDAV\FileRiseDirectory;
|
||||||
|
|
||||||
|
// ─── 3) HTTP‑Basic backend ─────────────────────────────────────────────────
|
||||||
$authBackend = new BasicCallBack(function(string $user, string $pass) {
|
$authBackend = new BasicCallBack(function(string $user, string $pass) {
|
||||||
return \AuthModel::authenticate($user, $pass) !== false;
|
return \AuthModel::authenticate($user, $pass) !== false;
|
||||||
});
|
});
|
||||||
$authPlugin = new AuthPlugin($authBackend, 'FileRise');
|
$authPlugin = new AuthPlugin($authBackend, 'FileRise');
|
||||||
|
|
||||||
|
// ─── 4) Determine user scope ────────────────────────────────────────────────
|
||||||
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
|
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
|
||||||
$isAdmin = (\AuthModel::getUserRole($user) === '1');
|
$isAdmin = (\AuthModel::getUserRole($user) === '1');
|
||||||
$folderOnly = (bool)\AuthModel::loadFolderPermission($user);
|
$folderOnly = (bool)\AuthModel::loadFolderPermission($user);
|
||||||
|
|
||||||
if ($isAdmin || !$folderOnly) {
|
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, '/\\');
|
$rootPath = rtrim(UPLOAD_DIR, '/\\');
|
||||||
} else {
|
} else {
|
||||||
// folder‑only users see only /uploads/{username}
|
// Folder‑only users see only /uploads/{username}
|
||||||
$rootPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $user;
|
$rootPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $user;
|
||||||
if (!is_dir($rootPath)) {
|
if (!is_dir($rootPath)) {
|
||||||
mkdir($rootPath, 0755, true);
|
mkdir($rootPath, 0755, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 5) Spin up SabreDAV ────────────────────────────────────────────────────
|
||||||
$server = new Server([
|
$server = new Server([
|
||||||
new FileRiseDirectory($rootPath, $user, $folderOnly),
|
new FileRiseDirectory($rootPath, $user, $folderOnly),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$server->addPlugin($authPlugin);
|
$server->addPlugin($authPlugin);
|
||||||
//$server->addPlugin(new BrowserPlugin()); // optional HTML browser UI
|
|
||||||
$server->addPlugin(
|
$server->addPlugin(
|
||||||
new LocksPlugin(
|
new LocksPlugin(
|
||||||
new LocksFileBackend(sys_get_temp_dir() . '/sabre-locksdb')
|
new LocksFileBackend(sys_get_temp_dir() . '/sabre-locksdb')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
// src/controllers/adminController.php
|
// src/controllers/AdminController.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/models/AdminModel.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="disableBasicAuth", type="boolean", example=false),
|
||||||
* @OA\Property(property="disableOIDCLogin", 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(
|
* @OA\Response(
|
||||||
@@ -88,7 +90,9 @@ class AdminController
|
|||||||
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
||||||
* @OA\Property(property="disableOIDCLogin", 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(
|
* @OA\Response(
|
||||||
@@ -149,7 +153,7 @@ class AdminController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare configuration array.
|
// Prepare existing settings
|
||||||
$headerTitle = isset($data['header_title']) ? trim($data['header_title']) : "";
|
$headerTitle = isset($data['header_title']) ? trim($data['header_title']) : "";
|
||||||
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
|
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
|
||||||
$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : '';
|
$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : '';
|
||||||
@@ -183,20 +187,38 @@ class AdminController
|
|||||||
}
|
}
|
||||||
$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : "";
|
$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 = [
|
$configUpdate = [
|
||||||
'header_title' => $headerTitle,
|
'header_title' => $headerTitle,
|
||||||
'oidc' => [
|
'oidc' => [
|
||||||
'providerUrl' => $oidcProviderUrl,
|
'providerUrl' => $oidcProviderUrl,
|
||||||
'clientId' => $oidcClientId,
|
'clientId' => $oidcClientId,
|
||||||
'clientSecret' => $oidcClientSecret,
|
'clientSecret' => $oidcClientSecret,
|
||||||
'redirectUri' => $oidcRedirectUri,
|
'redirectUri' => $oidcRedirectUri,
|
||||||
],
|
],
|
||||||
'loginOptions' => [
|
'loginOptions' => [
|
||||||
'disableFormLogin' => $disableFormLogin,
|
'disableFormLogin' => $disableFormLogin,
|
||||||
'disableBasicAuth' => $disableBasicAuth,
|
'disableBasicAuth' => $disableBasicAuth,
|
||||||
'disableOIDCLogin' => $disableOIDCLogin,
|
'disableOIDCLogin' => $disableOIDCLogin,
|
||||||
],
|
],
|
||||||
'globalOtpauthUrl' => $globalOtpauthUrl
|
'globalOtpauthUrl' => $globalOtpauthUrl,
|
||||||
|
'enableWebDAV' => $enableWebDAV,
|
||||||
|
'sharedMaxUploadSize' => $sharedMaxUploadSize // ← NEW
|
||||||
];
|
];
|
||||||
|
|
||||||
// Delegate to the model.
|
// Delegate to the model.
|
||||||
@@ -207,4 +229,4 @@ class AdminController
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
// src/controllers/authController.php
|
// src/controllers/AuthController.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/models/AuthModel.php';
|
require_once PROJECT_ROOT . '/src/models/AuthModel.php';
|
||||||
@@ -238,28 +238,28 @@ class AuthController
|
|||||||
$token = bin2hex(random_bytes(32));
|
$token = bin2hex(random_bytes(32));
|
||||||
$expiry = time() + 30 * 24 * 60 * 60;
|
$expiry = time() + 30 * 24 * 60 * 60;
|
||||||
$all = [];
|
$all = [];
|
||||||
|
|
||||||
if (file_exists($tokFile)) {
|
if (file_exists($tokFile)) {
|
||||||
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||||
$all = json_decode($dec, true) ?: [];
|
$all = json_decode($dec, true) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$all[$token] = [
|
$all[$token] = [
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'expiry' => $expiry,
|
'expiry' => $expiry,
|
||||||
'isAdmin' => $_SESSION['isAdmin']
|
'isAdmin' => $_SESSION['isAdmin']
|
||||||
];
|
];
|
||||||
|
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
$tokFile,
|
$tokFile,
|
||||||
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||||
LOCK_EX
|
LOCK_EX
|
||||||
);
|
);
|
||||||
|
|
||||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||||
|
|
||||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||||
|
|
||||||
setcookie(
|
setcookie(
|
||||||
session_name(),
|
session_name(),
|
||||||
session_id(),
|
session_id(),
|
||||||
@@ -269,7 +269,7 @@ class AuthController
|
|||||||
$secure,
|
$secure,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,40 +341,86 @@ class AuthController
|
|||||||
|
|
||||||
public function checkAuth(): void
|
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;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
|
||||||
// setup mode?
|
// 2) Setup mode?
|
||||||
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
|
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
|
||||||
error_log("checkAuth: setup mode");
|
error_log("checkAuth: setup mode");
|
||||||
echo json_encode(['setup' => true]);
|
echo json_encode(['setup' => true]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3) Session-based auth
|
||||||
if (empty($_SESSION['authenticated'])) {
|
if (empty($_SESSION['authenticated'])) {
|
||||||
echo json_encode(['authenticated' => false]);
|
echo json_encode(['authenticated' => false]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TOTP enabled?
|
// 4) TOTP enabled?
|
||||||
$totp = false;
|
$totp = false;
|
||||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
$parts = explode(':', trim($line));
|
$parts = explode(':', trim($line));
|
||||||
if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) {
|
if ($parts[0] === ($_SESSION['username'] ?? '') && !empty($parts[3])) {
|
||||||
$totp = true;
|
$totp = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$isAdmin = ((int)AuthModel::getUserRole($_SESSION['username']) === 1);
|
// 5) Final response
|
||||||
$resp = [
|
$resp = [
|
||||||
'authenticated' => true,
|
'authenticated' => true,
|
||||||
'isAdmin' => $isAdmin,
|
'isAdmin' => !empty($_SESSION['isAdmin']),
|
||||||
'totp_enabled' => $totp,
|
'totp_enabled' => $totp,
|
||||||
'username' => $_SESSION['username'],
|
'username' => $_SESSION['username'],
|
||||||
'folderOnly' => $_SESSION['folderOnly'] ?? false,
|
'folderOnly' => $_SESSION['folderOnly'] ?? false,
|
||||||
'readOnly' => $_SESSION['readOnly'] ?? false,
|
'readOnly' => $_SESSION['readOnly'] ?? false,
|
||||||
'disableUpload' => $_SESSION['disableUpload'] ?? false
|
'disableUpload' => $_SESSION['disableUpload'] ?? false
|
||||||
];
|
];
|
||||||
|
|
||||||
echo json_encode($resp);
|
echo json_encode($resp);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
@@ -403,10 +449,19 @@ class AuthController
|
|||||||
*/
|
*/
|
||||||
public function getToken(): void
|
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('Content-Type: application/json');
|
||||||
|
header('X-CSRF-Token: ' . $_SESSION['csrf_token']);
|
||||||
|
|
||||||
|
// 3) Return JSON payload
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"csrf_token" => $_SESSION['csrf_token'],
|
'csrf_token' => $_SESSION['csrf_token'],
|
||||||
"share_url" => SHARE_URL
|
'share_url' => SHARE_URL
|
||||||
]);
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
// src/controllers/folderController.php
|
// src/controllers/FolderController.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/models/FolderModel.php';
|
require_once PROJECT_ROOT . '/src/models/FolderModel.php';
|
||||||
@@ -76,7 +76,11 @@ class FolderController
|
|||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +405,20 @@ class FolderController
|
|||||||
*
|
*
|
||||||
* @return void Outputs HTML content.
|
* @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
|
public function shareFolder(): void
|
||||||
{
|
{
|
||||||
// Retrieve GET parameters.
|
// Retrieve GET parameters.
|
||||||
@@ -495,12 +513,14 @@ class FolderController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract data for the HTML view.
|
// Load admin config so we can pull the sharedMaxUploadSize
|
||||||
$folderName = $data['folder'];
|
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||||||
$files = $data['files'];
|
$adminConfig = AdminModel::getConfig();
|
||||||
$currentPage = $data['currentPage'];
|
$sharedMaxUploadSize = isset($adminConfig['sharedMaxUploadSize']) && is_numeric($adminConfig['sharedMaxUploadSize'])
|
||||||
$totalPages = $data['totalPages'];
|
? (int)$adminConfig['sharedMaxUploadSize']
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// For human‐readable formatting
|
||||||
function formatBytes($bytes)
|
function formatBytes($bytes)
|
||||||
{
|
{
|
||||||
if ($bytes < 1024) {
|
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.
|
// Build the HTML view.
|
||||||
header("Content-Type: text/html; charset=utf-8");
|
header("Content-Type: text/html; charset=utf-8");
|
||||||
?>
|
?>
|
||||||
@@ -528,13 +554,18 @@ class FolderController
|
|||||||
body {
|
body {
|
||||||
background: #f2f2f2;
|
background: #f2f2f2;
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
padding: 20px;
|
padding: 0px 20px 20px 20px;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@@ -639,6 +670,28 @@ class FolderController
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: #777;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -648,7 +701,7 @@ class FolderController
|
|||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Toggle Button -->
|
<!-- 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 -->
|
<!-- List View Container -->
|
||||||
<div id="listViewContainer">
|
<div id="listViewContainer">
|
||||||
@@ -717,7 +770,11 @@ class FolderController
|
|||||||
<!-- Upload Container (if uploads are allowed by the share record) -->
|
<!-- Upload Container (if uploads are allowed by the share record) -->
|
||||||
<?php if (isset($data['record']['allowUpload']) && $data['record']['allowUpload'] == 1): ?>
|
<?php if (isset($data['record']['allowUpload']) && $data['record']['allowUpload'] == 1): ?>
|
||||||
<div class="upload-container">
|
<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">
|
<form action="/api/folder/uploadToSharedFolder.php" method="post" enctype="multipart/form-data">
|
||||||
<!-- Pass the share token so the upload endpoint can verify -->
|
<!-- Pass the share token so the upload endpoint can verify -->
|
||||||
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
|
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
@@ -731,75 +788,14 @@ class FolderController
|
|||||||
<div class="footer">
|
<div class="footer">
|
||||||
© <?php echo date("Y"); ?> FileRise. All rights reserved.
|
© <?php echo date("Y"); ?> FileRise. All rights reserved.
|
||||||
</div>
|
</div>
|
||||||
|
<!-- non-executing JSON payload, never blocked by CSP -->
|
||||||
<script>
|
<script type="application/json" id="shared-data">
|
||||||
// (Optional) JavaScript for toggling view modes (list/gallery).
|
{
|
||||||
var viewMode = 'list';
|
"token": <?php echo json_encode($token, JSON_HEX_TAG); ?>,
|
||||||
window.imageCache = window.imageCache || {};
|
"files": <?php echo json_encode($files, JSON_HEX_TAG); ?>
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<script src="/js/sharedFolderView.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -855,38 +851,63 @@ class FolderController
|
|||||||
{
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Ensure user is authenticated.
|
// Auth check
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the user is not read-only.
|
// Read-only check
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
$userPermissions = loadUserPermissions($username);
|
$perms = loadUserPermissions($username);
|
||||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if ($username && !empty($perms['readOnly'])) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(["error" => "Read-only users are not allowed to create share folders."]);
|
echo json_encode(["error" => "Read-only users are not allowed to create share folders."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve and decode POST input.
|
// Input
|
||||||
$input = json_decode(file_get_contents("php://input"), true);
|
$in = json_decode(file_get_contents("php://input"), true);
|
||||||
if (!$input || !isset($input['folder'])) {
|
if (!$in || !isset($in['folder'])) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(["error" => "Invalid input."]);
|
echo json_encode(["error" => "Invalid input."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$folder = trim($input['folder']);
|
$folder = trim($in['folder']);
|
||||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
$value = isset($in['expirationValue']) ? intval($in['expirationValue']) : 60;
|
||||||
$password = isset($input['password']) ? $input['password'] : "";
|
$unit = $in['expirationUnit'] ?? 'minutes';
|
||||||
$allowUpload = isset($input['allowUpload']) ? intval($input['allowUpload']) : 0;
|
$password = $in['password'] ?? '';
|
||||||
|
$allowUpload = intval($in['allowUpload'] ?? 0);
|
||||||
|
|
||||||
// Delegate to the model.
|
// Folder name validation
|
||||||
$result = FolderModel::createShareFolderLink($folder, $expirationMinutes, $password, $allowUpload);
|
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||||
echo json_encode($result);
|
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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1057,4 +1078,34 @@ class FolderController
|
|||||||
header("Location: " . $redirectUrl);
|
header("Location: " . $redirectUrl);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/folder/getShareFolderLinks.php
|
||||||
|
*/
|
||||||
|
public function getShareFolderLinks()
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$links = FolderModel::getAllShareFolderLinks();
|
||||||
|
echo json_encode($links, JSON_PRETTY_PRINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
<?php
|
||||||
// src/controllers/uploadController.php
|
// src/controllers/UploadController.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/models/UploadModel.php';
|
require_once PROJECT_ROOT . '/src/models/UploadModel.php';
|
||||||
@@ -72,34 +72,56 @@ class UploadController {
|
|||||||
*/
|
*/
|
||||||
public function handleUpload(): void {
|
public function handleUpload(): void {
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// CSRF Protection.
|
//
|
||||||
|
// 1) CSRF – pull from header or POST fields
|
||||||
|
//
|
||||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
$receivedToken = $headersArr['x-csrf-token'] ?? '';
|
$received = '';
|
||||||
if (!isset($_SESSION['csrf_token']) || trim($receivedToken) !== $_SESSION['csrf_token']) {
|
if (!empty($headersArr['x-csrf-token'])) {
|
||||||
http_response_code(403);
|
$received = trim($headersArr['x-csrf-token']);
|
||||||
echo json_encode(["error" => "Invalid 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;
|
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);
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
// Check user permissions.
|
$userPerms = loadUserPermissions($_SESSION['username']);
|
||||||
$username = $_SESSION['username'] ?? '';
|
if (!empty($userPerms['disableUpload'])) {
|
||||||
$userPermissions = loadUserPermissions($username);
|
|
||||||
if ($username && !empty($userPermissions['disableUpload'])) {
|
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(["error" => "Upload disabled for this user."]);
|
echo json_encode(["error" => "Upload disabled for this user."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegate to the model.
|
//
|
||||||
|
// 3) Delegate the actual file handling
|
||||||
|
//
|
||||||
$result = UploadModel::handleUpload($_POST, $_FILES);
|
$result = UploadModel::handleUpload($_POST, $_FILES);
|
||||||
|
|
||||||
// For chunked uploads, output JSON (e.g., "chunk uploaded" status).
|
//
|
||||||
|
// 4) Respond
|
||||||
|
//
|
||||||
if (isset($result['error'])) {
|
if (isset($result['error'])) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
@@ -109,8 +131,8 @@ class UploadController {
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, for full upload success, set a flash message and redirect.
|
// full‐upload redirect
|
||||||
$_SESSION['upload_message'] = "File uploaded successfully.";
|
$_SESSION['upload_message'] = "File uploaded successfully.";
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
// userController.php located in src/controllers/
|
// UserController.php located in src/controllers/
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
||||||
@@ -87,63 +87,83 @@ class UserController
|
|||||||
|
|
||||||
public function addUser()
|
public function addUser()
|
||||||
{
|
{
|
||||||
|
// 1) Ensure JSON output and session
|
||||||
header('Content-Type: application/json');
|
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.
|
// 2) Determine setup mode (first-ever admin creation)
|
||||||
// Setup mode means the "setup" query parameter is passed
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
// and users.txt is missing, empty, or contains only whitespace.
|
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
|
||||||
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
|
$setupMode = false;
|
||||||
if ($isSetup && (!file_exists($usersFile) || filesize($usersFile) == 0 || trim(file_get_contents($usersFile)) === '')) {
|
if (
|
||||||
// Allow initial admin creation without session or CSRF checks.
|
$isSetup && (! file_exists($usersFile)
|
||||||
|
|| filesize($usersFile) === 0
|
||||||
|
|| trim(file_get_contents($usersFile)) === ''
|
||||||
|
)
|
||||||
|
) {
|
||||||
$setupMode = true;
|
$setupMode = true;
|
||||||
} else {
|
} else {
|
||||||
$setupMode = false;
|
// 3) In non-setup, enforce CSRF + auth checks
|
||||||
// In non-setup mode, perform CSRF token and authentication checks.
|
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
$receivedToken = trim($headersArr['x-csrf-token'] ?? '');
|
||||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
|
||||||
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
// 3a) Soft-fail CSRF: on mismatch, regenerate and return new token
|
||||||
http_response_code(403);
|
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||||
echo json_encode(["error" => "Invalid 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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3b) Must be logged in as admin
|
||||||
if (
|
if (
|
||||||
!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
empty($_SESSION['authenticated'])
|
||||||
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true
|
|| $_SESSION['authenticated'] !== true
|
||||||
|
|| empty($_SESSION['isAdmin'])
|
||||||
|
|| $_SESSION['isAdmin'] !== true
|
||||||
) {
|
) {
|
||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the JSON input data.
|
// 4) Parse input
|
||||||
$data = json_decode(file_get_contents("php://input"), true);
|
$data = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||||
$newUsername = trim($data["username"] ?? "");
|
$newUsername = trim($data['username'] ?? '');
|
||||||
$newPassword = trim($data["password"] ?? "");
|
$newPassword = trim($data['password'] ?? '');
|
||||||
|
|
||||||
// In setup mode, force the new user to be an admin.
|
// 5) Determine admin flag
|
||||||
if ($setupMode) {
|
if ($setupMode) {
|
||||||
$isAdmin = "1";
|
$isAdmin = '1';
|
||||||
} else {
|
} else {
|
||||||
$isAdmin = !empty($data["isAdmin"]) ? "1" : "0";
|
$isAdmin = !empty($data['isAdmin']) ? '1' : '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that a username and password are provided.
|
// 6) Validate fields
|
||||||
if (!$newUsername || !$newPassword) {
|
if ($newUsername === '' || $newPassword === '') {
|
||||||
echo json_encode(["error" => "Username and password required"]);
|
echo json_encode(["error" => "Username and password required"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate username format.
|
|
||||||
if (!preg_match(REGEX_USER, $newUsername)) {
|
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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegate the business logic to the model.
|
// 7) Delegate to model
|
||||||
$result = userModel::addUser($newUsername, $newPassword, $isAdmin, $setupMode);
|
$result = userModel::addUser($newUsername, $newPassword, $isAdmin, $setupMode);
|
||||||
|
|
||||||
|
// 8) Return model result
|
||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -852,7 +872,7 @@ class UserController
|
|||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
||||||
|
|
||||||
// Rate‑limit
|
// Rate-limit
|
||||||
if (!isset($_SESSION['totp_failures'])) {
|
if (!isset($_SESSION['totp_failures'])) {
|
||||||
$_SESSION['totp_failures'] = 0;
|
$_SESSION['totp_failures'] = 0;
|
||||||
}
|
}
|
||||||
@@ -863,7 +883,7 @@ class UserController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must be authenticated OR pending login
|
// 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);
|
http_response_code(403);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
|
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
|
||||||
exit;
|
exit;
|
||||||
@@ -878,7 +898,7 @@ class UserController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and validate input
|
// Parse & validate input
|
||||||
$inputData = json_decode(file_get_contents("php://input"), true);
|
$inputData = json_decode(file_get_contents("php://input"), true);
|
||||||
$code = trim($inputData['totp_code'] ?? '');
|
$code = trim($inputData['totp_code'] ?? '');
|
||||||
if (!preg_match('/^\d{6}$/', $code)) {
|
if (!preg_match('/^\d{6}$/', $code)) {
|
||||||
@@ -893,11 +913,11 @@ class UserController
|
|||||||
'FileRise', 6, 30, \RobThree\Auth\Algorithm::Sha1
|
'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'])) {
|
if (isset($_SESSION['pending_login_user'])) {
|
||||||
$username = $_SESSION['pending_login_user'];
|
$username = $_SESSION['pending_login_user'];
|
||||||
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
|
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
|
||||||
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
|
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
|
||||||
|
|
||||||
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
|
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
|
||||||
$_SESSION['totp_failures']++;
|
$_SESSION['totp_failures']++;
|
||||||
@@ -906,53 +926,45 @@ class UserController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Issue “remember me” token if requested ===
|
// Issue “remember me” token if requested
|
||||||
if ($rememberMe) {
|
if ($rememberMe) {
|
||||||
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||||
$token = bin2hex(random_bytes(32));
|
$token = bin2hex(random_bytes(32));
|
||||||
$expiry = time() + 30 * 24 * 60 * 60;
|
$expiry = time() + 30 * 24 * 60 * 60;
|
||||||
$all = [];
|
$all = [];
|
||||||
|
|
||||||
if (file_exists($tokFile)) {
|
if (file_exists($tokFile)) {
|
||||||
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||||
$all = json_decode($dec, true) ?: [];
|
$all = json_decode($dec, true) ?: [];
|
||||||
}
|
}
|
||||||
$all[$token] = [
|
$all[$token] = [
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'expiry' => $expiry,
|
'expiry' => $expiry,
|
||||||
'isAdmin' => $_SESSION['isAdmin']
|
'isAdmin' => ((int)userModel::getUserRole($username) === 1),
|
||||||
|
'folderOnly' => loadUserPermissions($username)['folderOnly'] ?? false,
|
||||||
|
'readOnly' => loadUserPermissions($username)['readOnly'] ?? false,
|
||||||
|
'disableUpload'=> loadUserPermissions($username)['disableUpload']?? false
|
||||||
];
|
];
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
$tokFile,
|
$tokFile,
|
||||||
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||||
LOCK_EX
|
LOCK_EX
|
||||||
);
|
);
|
||||||
|
|
||||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||||
|
|
||||||
// Persistent cookie
|
|
||||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||||
|
setcookie(session_name(), session_id(), $expiry, '/', '', $secure, true);
|
||||||
// Re‑issue PHP session cookie
|
|
||||||
setcookie(
|
|
||||||
session_name(),
|
|
||||||
session_id(),
|
|
||||||
$expiry,
|
|
||||||
'/',
|
|
||||||
'',
|
|
||||||
$secure,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finalize login
|
// === Finalize login into session exactly as finalizeLogin() would ===
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
$_SESSION['authenticated'] = true;
|
$_SESSION['authenticated'] = true;
|
||||||
$_SESSION['username'] = $username;
|
$_SESSION['username'] = $username;
|
||||||
$_SESSION['isAdmin'] = (userModel::getUserRole($username) === "1");
|
$_SESSION['isAdmin'] = ((int)userModel::getUserRole($username) === 1);
|
||||||
$_SESSION['folderOnly'] = loadUserPermissions($username);
|
$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(
|
unset(
|
||||||
$_SESSION['pending_login_user'],
|
$_SESSION['pending_login_user'],
|
||||||
$_SESSION['pending_login_secret'],
|
$_SESSION['pending_login_secret'],
|
||||||
@@ -960,34 +972,43 @@ class UserController
|
|||||||
$_SESSION['totp_failures']
|
$_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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup/verification flow (not pending)
|
// Setup/verification flow (not pending)
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
if (!$username) {
|
if (!$username) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Username not found in session']);
|
echo json_encode(['status' => 'error', 'message' => 'Username not found in session']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$totpSecret = userModel::getTOTPSecret($username);
|
$totpSecret = userModel::getTOTPSecret($username);
|
||||||
if (!$totpSecret) {
|
if (!$totpSecret) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
|
echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$tfa->verifyCode($totpSecret, $code)) {
|
if (!$tfa->verifyCode($totpSecret, $code)) {
|
||||||
$_SESSION['totp_failures']++;
|
$_SESSION['totp_failures']++;
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Successful setup/verification
|
// Successful setup/verification
|
||||||
unset($_SESSION['totp_failures']);
|
unset($_SESSION['totp_failures']);
|
||||||
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,23 @@ require_once PROJECT_ROOT . '/config/config.php';
|
|||||||
|
|
||||||
class AdminModel
|
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.
|
* Updates the admin configuration file.
|
||||||
@@ -24,6 +41,28 @@ class AdminModel
|
|||||||
return ["error" => "Incomplete OIDC configuration."];
|
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.
|
// Convert configuration to JSON.
|
||||||
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
||||||
if ($plainTextConfig === false) {
|
if ($plainTextConfig === false) {
|
||||||
@@ -59,7 +98,8 @@ class AdminModel
|
|||||||
*
|
*
|
||||||
* @return array The configuration array, or defaults if not found.
|
* @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';
|
$configFile = USERS_DIR . 'adminConfig.json';
|
||||||
if (file_exists($configFile)) {
|
if (file_exists($configFile)) {
|
||||||
$encryptedContent = file_get_contents($configFile);
|
$encryptedContent = file_get_contents($configFile);
|
||||||
@@ -72,10 +112,9 @@ class AdminModel
|
|||||||
if (!is_array($config)) {
|
if (!is_array($config)) {
|
||||||
$config = [];
|
$config = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize login options.
|
// Normalize login options if missing
|
||||||
if (!isset($config['loginOptions'])) {
|
if (!isset($config['loginOptions'])) {
|
||||||
// Create loginOptions array from top-level keys if missing.
|
|
||||||
$config['loginOptions'] = [
|
$config['loginOptions'] = [
|
||||||
'disableFormLogin' => isset($config['disableFormLogin']) ? (bool)$config['disableFormLogin'] : false,
|
'disableFormLogin' => isset($config['disableFormLogin']) ? (bool)$config['disableFormLogin'] : false,
|
||||||
'disableBasicAuth' => isset($config['disableBasicAuth']) ? (bool)$config['disableBasicAuth'] : false,
|
'disableBasicAuth' => isset($config['disableBasicAuth']) ? (bool)$config['disableBasicAuth'] : false,
|
||||||
@@ -88,31 +127,43 @@ class AdminModel
|
|||||||
$config['loginOptions']['disableBasicAuth'] = (bool)$config['loginOptions']['disableBasicAuth'];
|
$config['loginOptions']['disableBasicAuth'] = (bool)$config['loginOptions']['disableBasicAuth'];
|
||||||
$config['loginOptions']['disableOIDCLogin'] = (bool)$config['loginOptions']['disableOIDCLogin'];
|
$config['loginOptions']['disableOIDCLogin'] = (bool)$config['loginOptions']['disableOIDCLogin'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default values for other keys
|
||||||
if (!isset($config['globalOtpauthUrl'])) {
|
if (!isset($config['globalOtpauthUrl'])) {
|
||||||
$config['globalOtpauthUrl'] = "";
|
$config['globalOtpauthUrl'] = "";
|
||||||
}
|
}
|
||||||
if (!isset($config['header_title']) || empty($config['header_title'])) {
|
if (!isset($config['header_title']) || empty($config['header_title'])) {
|
||||||
$config['header_title'] = "FileRise";
|
$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;
|
return $config;
|
||||||
} else {
|
} else {
|
||||||
// Return defaults.
|
// Return defaults.
|
||||||
return [
|
return [
|
||||||
'header_title' => "FileRise",
|
'header_title' => "FileRise",
|
||||||
'oidc' => [
|
'oidc' => [
|
||||||
'providerUrl' => 'https://your-oidc-provider.com',
|
'providerUrl' => 'https://your-oidc-provider.com',
|
||||||
'clientId' => 'YOUR_CLIENT_ID',
|
'clientId' => 'YOUR_CLIENT_ID',
|
||||||
'clientSecret' => 'YOUR_CLIENT_SECRET',
|
'clientSecret' => 'YOUR_CLIENT_SECRET',
|
||||||
'redirectUri' => 'https://yourdomain.com/api/auth/auth.php?oidc=callback'
|
'redirectUri' => 'https://yourdomain.com/api/auth/auth.php?oidc=callback'
|
||||||
],
|
],
|
||||||
'loginOptions' => [
|
'loginOptions' => [
|
||||||
'disableFormLogin' => false,
|
'disableFormLogin' => false,
|
||||||
'disableBasicAuth' => false,
|
'disableBasicAuth' => false,
|
||||||
'disableOIDCLogin' => 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';
|
require_once PROJECT_ROOT . '/config/config.php';
|
||||||
|
|
||||||
class AuthModel {
|
class AuthModel
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the user's role from the users file.
|
* Retrieves the user's role from the users file.
|
||||||
@@ -11,7 +12,8 @@ class AuthModel {
|
|||||||
* @param string $username
|
* @param string $username
|
||||||
* @return string|null The role string (e.g. "1" for admin) or null if not found.
|
* @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;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (file_exists($usersFile)) {
|
if (file_exists($usersFile)) {
|
||||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
@@ -23,7 +25,7 @@ class AuthModel {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticates the user using form-based credentials.
|
* Authenticates the user using form-based credentials.
|
||||||
*
|
*
|
||||||
@@ -31,7 +33,8 @@ class AuthModel {
|
|||||||
* @param string $password
|
* @param string $password
|
||||||
* @return array|false Returns an associative array with user data (role, totp_secret) on success or false on failure.
|
* @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;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -51,14 +54,15 @@ class AuthModel {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads failed login attempts from a file.
|
* Loads failed login attempts from a file.
|
||||||
*
|
*
|
||||||
* @param string $file
|
* @param string $file
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function loadFailedAttempts(string $file): array {
|
public static function loadFailedAttempts(string $file): array
|
||||||
|
{
|
||||||
if (file_exists($file)) {
|
if (file_exists($file)) {
|
||||||
$data = json_decode(file_get_contents($file), true);
|
$data = json_decode(file_get_contents($file), true);
|
||||||
if (is_array($data)) {
|
if (is_array($data)) {
|
||||||
@@ -67,7 +71,7 @@ class AuthModel {
|
|||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves failed login attempts into a file.
|
* Saves failed login attempts into a file.
|
||||||
*
|
*
|
||||||
@@ -75,17 +79,19 @@ class AuthModel {
|
|||||||
* @param array $data
|
* @param array $data
|
||||||
* @return void
|
* @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);
|
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a user's TOTP secret from the users file.
|
* Retrieves a user's TOTP secret from the users file.
|
||||||
*
|
*
|
||||||
* @param string $username
|
* @param string $username
|
||||||
* @return string|null Returns the decrypted TOTP secret or null if not set.
|
* @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;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -98,14 +104,15 @@ class AuthModel {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the folder-only permission for a given user.
|
* Loads the folder-only permission for a given user.
|
||||||
*
|
*
|
||||||
* @param string $username
|
* @param string $username
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function loadFolderPermission(string $username): bool {
|
public static function loadFolderPermission(string $username): bool
|
||||||
|
{
|
||||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||||
if (file_exists($permissionsFile)) {
|
if (file_exists($permissionsFile)) {
|
||||||
$content = file_get_contents($permissionsFile);
|
$content = file_get_contents($permissionsFile);
|
||||||
@@ -121,4 +128,31 @@ class AuthModel {
|
|||||||
}
|
}
|
||||||
return false;
|
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,
|
* @return array Returns an associative array with keys "token" and "expires" on success,
|
||||||
* or "error" on failure.
|
* 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).
|
// Validate folder if necessary (this can also be done in the controller).
|
||||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||||
return ["error" => "Invalid folder name."];
|
return ["error" => "Invalid folder name."];
|
||||||
@@ -746,7 +746,7 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
|||||||
$token = bin2hex(random_bytes(16));
|
$token = bin2hex(random_bytes(16));
|
||||||
|
|
||||||
// Calculate expiration (Unix timestamp).
|
// Calculate expiration (Unix timestamp).
|
||||||
$expires = time() + ($expirationMinutes * 60);
|
$expires = time() + $expirationSeconds;
|
||||||
|
|
||||||
// Hash the password if provided.
|
// Hash the password if provided.
|
||||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
$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];
|
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';
|
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.
|
* 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,
|
* @return array Returns an array with a "success" key if the folder was created,
|
||||||
* or an "error" key if an error occurred.
|
* 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);
|
$folderName = trim($folderName);
|
||||||
$parent = trim($parent);
|
$parent = trim($parent);
|
||||||
|
|
||||||
// Validate folder name (only letters, numbers, underscores, dashes, and spaces allowed).
|
// Validate folder name (only letters, numbers, underscores, dashes, and spaces allowed).
|
||||||
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
||||||
return ["error" => "Invalid folder name."];
|
return ["error" => "Invalid folder name."];
|
||||||
@@ -23,7 +25,7 @@ class FolderModel {
|
|||||||
if ($parent !== "" && !preg_match(REGEX_FOLDER_NAME, $parent)) {
|
if ($parent !== "" && !preg_match(REGEX_FOLDER_NAME, $parent)) {
|
||||||
return ["error" => "Invalid parent folder name."];
|
return ["error" => "Invalid parent folder name."];
|
||||||
}
|
}
|
||||||
|
|
||||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||||
if ($parent !== "" && strtolower($parent) !== "root") {
|
if ($parent !== "" && strtolower($parent) !== "root") {
|
||||||
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $parent . DIRECTORY_SEPARATOR . $folderName;
|
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $parent . DIRECTORY_SEPARATOR . $folderName;
|
||||||
@@ -32,12 +34,12 @@ class FolderModel {
|
|||||||
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $folderName;
|
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $folderName;
|
||||||
$relativePath = $folderName;
|
$relativePath = $folderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the folder already exists.
|
// Check if the folder already exists.
|
||||||
if (file_exists($fullPath)) {
|
if (file_exists($fullPath)) {
|
||||||
return ["error" => "Folder already exists."];
|
return ["error" => "Folder already exists."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to create the folder.
|
// Attempt to create the folder.
|
||||||
if (mkdir($fullPath, 0755, true)) {
|
if (mkdir($fullPath, 0755, true)) {
|
||||||
// Create an empty metadata file for the new folder.
|
// Create an empty metadata file for the new folder.
|
||||||
@@ -50,52 +52,54 @@ class FolderModel {
|
|||||||
return ["error" => "Failed to create folder."];
|
return ["error" => "Failed to create folder."];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the metadata file path for a given folder.
|
* Generates the metadata file path for a given folder.
|
||||||
*
|
*
|
||||||
* @param string $folder The relative folder path.
|
* @param string $folder The relative folder path.
|
||||||
* @return string The metadata file 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) === '') {
|
if (strtolower($folder) === 'root' || trim($folder) === '') {
|
||||||
return META_DIR . "root_metadata.json";
|
return META_DIR . "root_metadata.json";
|
||||||
}
|
}
|
||||||
return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
|
return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a folder if it is empty and removes its corresponding metadata.
|
* Deletes a folder if it is empty and removes its corresponding metadata.
|
||||||
*
|
*
|
||||||
* @param string $folder The folder name (relative to the upload directory).
|
* @param string $folder The folder name (relative to the upload directory).
|
||||||
* @return array An associative array with "success" on success or "error" on failure.
|
* @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".
|
// Prevent deletion of "root".
|
||||||
if (strtolower($folder) === 'root') {
|
if (strtolower($folder) === 'root') {
|
||||||
return ["error" => "Cannot delete root folder."];
|
return ["error" => "Cannot delete root folder."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate folder name.
|
// Validate folder name.
|
||||||
if (!preg_match(REGEX_FOLDER_NAME, $folder)) {
|
if (!preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||||
return ["error" => "Invalid folder name."];
|
return ["error" => "Invalid folder name."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the full folder path.
|
// Build the full folder path.
|
||||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||||
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $folder;
|
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $folder;
|
||||||
|
|
||||||
// Check if the folder exists and is a directory.
|
// Check if the folder exists and is a directory.
|
||||||
if (!file_exists($folderPath) || !is_dir($folderPath)) {
|
if (!file_exists($folderPath) || !is_dir($folderPath)) {
|
||||||
return ["error" => "Folder does not exist."];
|
return ["error" => "Folder does not exist."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent deletion if the folder is not empty.
|
// Prevent deletion if the folder is not empty.
|
||||||
$items = array_diff(scandir($folderPath), array('.', '..'));
|
$items = array_diff(scandir($folderPath), array('.', '..'));
|
||||||
if (count($items) > 0) {
|
if (count($items) > 0) {
|
||||||
return ["error" => "Folder is not empty."];
|
return ["error" => "Folder is not empty."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to delete the folder.
|
// Attempt to delete the folder.
|
||||||
if (rmdir($folderPath)) {
|
if (rmdir($folderPath)) {
|
||||||
// Remove corresponding metadata file.
|
// Remove corresponding metadata file.
|
||||||
@@ -109,43 +113,45 @@ class FolderModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renames a folder and updates related metadata files.
|
* Renames a folder and updates related metadata files.
|
||||||
*
|
*
|
||||||
* @param string $oldFolder The current folder name (relative to UPLOAD_DIR).
|
* @param string $oldFolder The current folder name (relative to UPLOAD_DIR).
|
||||||
* @param string $newFolder The new folder name.
|
* @param string $newFolder The new folder name.
|
||||||
* @return array Returns an associative array with "success" on success or "error" on failure.
|
* @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.
|
// Sanitize and trim folder names.
|
||||||
$oldFolder = trim($oldFolder, "/\\ ");
|
$oldFolder = trim($oldFolder, "/\\ ");
|
||||||
$newFolder = trim($newFolder, "/\\ ");
|
$newFolder = trim($newFolder, "/\\ ");
|
||||||
|
|
||||||
// Validate folder names.
|
// Validate folder names.
|
||||||
if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) {
|
if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) {
|
||||||
return ["error" => "Invalid folder name(s)."];
|
return ["error" => "Invalid folder name(s)."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the full folder paths.
|
// Build the full folder paths.
|
||||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||||
$oldPath = $baseDir . DIRECTORY_SEPARATOR . $oldFolder;
|
$oldPath = $baseDir . DIRECTORY_SEPARATOR . $oldFolder;
|
||||||
$newPath = $baseDir . DIRECTORY_SEPARATOR . $newFolder;
|
$newPath = $baseDir . DIRECTORY_SEPARATOR . $newFolder;
|
||||||
|
|
||||||
// Validate that the old folder exists and new folder does not.
|
// Validate that the old folder exists and new folder does not.
|
||||||
if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) ||
|
if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) ||
|
||||||
strpos(realpath($oldPath), realpath($baseDir)) !== 0 ||
|
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."];
|
return ["error" => "Invalid folder path."];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file_exists($oldPath) || !is_dir($oldPath)) {
|
if (!file_exists($oldPath) || !is_dir($oldPath)) {
|
||||||
return ["error" => "Folder to rename does not exist."];
|
return ["error" => "Folder to rename does not exist."];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file_exists($newPath)) {
|
if (file_exists($newPath)) {
|
||||||
return ["error" => "New folder name already exists."];
|
return ["error" => "New folder name already exists."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to rename the folder.
|
// Attempt to rename the folder.
|
||||||
if (rename($oldPath, $newPath)) {
|
if (rename($oldPath, $newPath)) {
|
||||||
// Update metadata: Rename all metadata files that have the old folder prefix.
|
// 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.
|
* @param string $relative The relative path from the base directory.
|
||||||
* @return array An array of folder paths (relative to the base).
|
* @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 = [];
|
$folders = [];
|
||||||
$items = scandir($dir);
|
$items = scandir($dir);
|
||||||
$safeFolderNamePattern = REGEX_FOLDER_NAME;
|
$safeFolderNamePattern = REGEX_FOLDER_NAME;
|
||||||
@@ -198,7 +205,8 @@ class FolderModel {
|
|||||||
*
|
*
|
||||||
* @return array An array of folder information arrays.
|
* @return array An array of folder information arrays.
|
||||||
*/
|
*/
|
||||||
public static function getFolderList(): array {
|
public static function getFolderList(): array
|
||||||
|
{
|
||||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||||
$folderInfoList = [];
|
$folderInfoList = [];
|
||||||
|
|
||||||
@@ -240,13 +248,14 @@ class FolderModel {
|
|||||||
return $folderInfoList;
|
return $folderInfoList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the share folder record for a given token.
|
* Retrieves the share folder record for a given token.
|
||||||
*
|
*
|
||||||
* @param string $token The share folder token.
|
* @param string $token The share folder token.
|
||||||
* @return array|null The share folder record, or null if not found.
|
* @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";
|
$shareFile = META_DIR . "share_folder_links.json";
|
||||||
if (!file_exists($shareFile)) {
|
if (!file_exists($shareFile)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -257,8 +266,8 @@ class FolderModel {
|
|||||||
}
|
}
|
||||||
return $shareLinks[$token];
|
return $shareLinks[$token];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves shared folder data based on a share token.
|
* Retrieves shared folder data based on a share token.
|
||||||
*
|
*
|
||||||
* @param string $token The share folder token.
|
* @param string $token The share folder token.
|
||||||
@@ -274,7 +283,8 @@ class FolderModel {
|
|||||||
* - 'totalPages': total pages,
|
* - 'totalPages': total pages,
|
||||||
* or an 'error' key on failure.
|
* 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.
|
// Load the share folder record.
|
||||||
$shareFile = META_DIR . "share_folder_links.json";
|
$shareFile = META_DIR . "share_folder_links.json";
|
||||||
if (!file_exists($shareFile)) {
|
if (!file_exists($shareFile)) {
|
||||||
@@ -314,7 +324,7 @@ class FolderModel {
|
|||||||
return ["error" => "Shared folder not found."];
|
return ["error" => "Shared folder not found."];
|
||||||
}
|
}
|
||||||
// Scan for files (only files).
|
// 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);
|
return is_file($realFolderPath . DIRECTORY_SEPARATOR . $item);
|
||||||
}));
|
}));
|
||||||
sort($allFiles);
|
sort($allFiles);
|
||||||
@@ -323,7 +333,7 @@ class FolderModel {
|
|||||||
$currentPage = min($page, $totalPages);
|
$currentPage = min($page, $totalPages);
|
||||||
$startIndex = ($currentPage - 1) * $itemsPerPage;
|
$startIndex = ($currentPage - 1) * $itemsPerPage;
|
||||||
$filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage);
|
$filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"record" => $record,
|
"record" => $record,
|
||||||
"folder" => $folder,
|
"folder" => $folder,
|
||||||
@@ -334,81 +344,72 @@ class FolderModel {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a share link for a folder.
|
* Creates a share link for a folder.
|
||||||
*
|
*
|
||||||
* @param string $folder The folder to share (relative to UPLOAD_DIR).
|
* @param string $folder The folder to share (relative to UPLOAD_DIR).
|
||||||
* @param int $expirationMinutes The duration (in minutes) until the link expires.
|
* @param int $expirationSeconds How many seconds until expiry.
|
||||||
* @param string $password Optional password for the share.
|
* @param string $password Optional password.
|
||||||
* @param int $allowUpload Optional flag (0 or 1) indicating whether uploads are allowed.
|
* @param int $allowUpload 0 or 1 whether uploads are allowed.
|
||||||
* @return array An associative array with "token", "expires", and "link" on success, or "error" on failure.
|
* @return array ["token","expires","link"] on success, or ["error"].
|
||||||
*/
|
*/
|
||||||
public static function createShareFolderLink(string $folder, int $expirationMinutes = 60, string $password = "", int $allowUpload = 0): array {
|
public static function createShareFolderLink(string $folder, int $expirationSeconds = 3600, string $password = "", int $allowUpload = 0): array
|
||||||
// Validate folder name.
|
{
|
||||||
|
// Validate folder
|
||||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||||
return ["error" => "Invalid folder name."];
|
return ["error" => "Invalid folder name."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate secure token.
|
// Token
|
||||||
try {
|
try {
|
||||||
$token = bin2hex(random_bytes(16)); // 32 hex characters.
|
$token = bin2hex(random_bytes(16));
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
return ["error" => "Could not generate token."];
|
return ["error" => "Could not generate token."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate expiration time.
|
// Expiry
|
||||||
$expires = time() + ($expirationMinutes * 60);
|
$expires = time() + $expirationSeconds;
|
||||||
|
|
||||||
// Hash the password if provided.
|
// Password hash
|
||||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
$hashedPassword = $password !== "" ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||||
|
|
||||||
// Define the share folder links file.
|
// Load existing
|
||||||
$shareFile = META_DIR . "share_folder_links.json";
|
$shareFile = META_DIR . "share_folder_links.json";
|
||||||
$shareLinks = [];
|
$links = file_exists($shareFile)
|
||||||
if (file_exists($shareFile)) {
|
? json_decode(file_get_contents($shareFile), true) ?? []
|
||||||
$data = file_get_contents($shareFile);
|
: [];
|
||||||
$shareLinks = json_decode($data, true);
|
|
||||||
if (!is_array($shareLinks)) {
|
// Cleanup
|
||||||
$shareLinks = [];
|
$now = time();
|
||||||
|
foreach ($links as $k => $v) {
|
||||||
|
if (!empty($v['expires']) && $v['expires'] < $now) {
|
||||||
|
unset($links[$k]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up expired share links.
|
// Add new
|
||||||
$currentTime = time();
|
$links[$token] = [
|
||||||
foreach ($shareLinks as $key => $link) {
|
"folder" => $folder,
|
||||||
if (isset($link["expires"]) && $link["expires"] < $currentTime) {
|
"expires" => $expires,
|
||||||
unset($shareLinks[$key]);
|
"password" => $hashedPassword,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new share record.
|
|
||||||
$shareLinks[$token] = [
|
|
||||||
"folder" => $folder,
|
|
||||||
"expires" => $expires,
|
|
||||||
"password" => $hashedPassword,
|
|
||||||
"allowUpload" => $allowUpload
|
"allowUpload" => $allowUpload
|
||||||
];
|
];
|
||||||
|
|
||||||
// Save the updated share links.
|
// Save
|
||||||
if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT)) === false) {
|
if (file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT)) === false) {
|
||||||
return ["error" => "Could not save share link."];
|
return ["error" => "Could not save share link."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the base URL.
|
// Build URL
|
||||||
if (defined('BASE_URL') && !empty(BASE_URL) && strpos(BASE_URL, 'yourwebsite') === false) {
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||||
$baseUrl = rtrim(BASE_URL, '/');
|
$host = $_SERVER['HTTP_HOST'] ?? gethostbyname(gethostname());
|
||||||
} else {
|
$baseUrl = $protocol . '://' . rtrim($host, '/');
|
||||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
$link = $baseUrl . "/api/folder/shareFolder.php?token=" . urlencode($token);
|
||||||
$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);
|
|
||||||
|
|
||||||
return ["token" => $token, "expires" => $expires, "link" => $link];
|
return ["token" => $token, "expires" => $expires, "link" => $link];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves information for a shared file from a shared folder link.
|
* Retrieves information for a shared file from a shared folder link.
|
||||||
*
|
*
|
||||||
* @param string $token The share folder token.
|
* @param string $token The share folder token.
|
||||||
@@ -418,7 +419,8 @@ class FolderModel {
|
|||||||
* - "realFilePath": the absolute path to the file,
|
* - "realFilePath": the absolute path to the file,
|
||||||
* - "mimeType": the detected MIME type.
|
* - "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.
|
// Load the share folder record.
|
||||||
$shareFile = META_DIR . "share_folder_links.json";
|
$shareFile = META_DIR . "share_folder_links.json";
|
||||||
if (!file_exists($shareFile)) {
|
if (!file_exists($shareFile)) {
|
||||||
@@ -457,14 +459,14 @@ class FolderModel {
|
|||||||
return ["error" => "Invalid file name."];
|
return ["error" => "Invalid file name."];
|
||||||
}
|
}
|
||||||
$file = basename($file);
|
$file = basename($file);
|
||||||
|
|
||||||
// Build the full file path.
|
// Build the full file path.
|
||||||
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
|
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
|
||||||
$realFilePath = realpath($filePath);
|
$realFilePath = realpath($filePath);
|
||||||
if ($realFilePath === false || strpos($realFilePath, $realFolderPath) !== 0 || !is_file($realFilePath)) {
|
if ($realFilePath === false || strpos($realFilePath, $realFolderPath) !== 0 || !is_file($realFilePath)) {
|
||||||
return ["error" => "File not found."];
|
return ["error" => "File not found."];
|
||||||
}
|
}
|
||||||
|
|
||||||
$mimeType = mime_content_type($realFilePath);
|
$mimeType = mime_content_type($realFilePath);
|
||||||
return [
|
return [
|
||||||
"realFilePath" => $realFilePath,
|
"realFilePath" => $realFilePath,
|
||||||
@@ -479,11 +481,12 @@ class FolderModel {
|
|||||||
* @param array $fileUpload The $_FILES['fileToUpload'] array.
|
* @param array $fileUpload The $_FILES['fileToUpload'] array.
|
||||||
* @return array An associative array with "success" on success or "error" on failure.
|
* @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.
|
// Define maximum file size and allowed extensions.
|
||||||
$maxSize = 50 * 1024 * 1024; // 50 MB
|
$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.
|
// Load the share folder record.
|
||||||
$shareFile = META_DIR . "share_folder_links.json";
|
$shareFile = META_DIR . "share_folder_links.json";
|
||||||
if (!file_exists($shareFile)) {
|
if (!file_exists($shareFile)) {
|
||||||
@@ -494,55 +497,55 @@ class FolderModel {
|
|||||||
return ["error" => "Invalid share token."];
|
return ["error" => "Invalid share token."];
|
||||||
}
|
}
|
||||||
$record = $shareLinks[$token];
|
$record = $shareLinks[$token];
|
||||||
|
|
||||||
// Check expiration.
|
// Check expiration.
|
||||||
if (time() > $record['expires']) {
|
if (time() > $record['expires']) {
|
||||||
return ["error" => "This share link has expired."];
|
return ["error" => "This share link has expired."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether uploads are allowed.
|
// Check whether uploads are allowed.
|
||||||
if (empty($record['allowUpload']) || $record['allowUpload'] != 1) {
|
if (empty($record['allowUpload']) || $record['allowUpload'] != 1) {
|
||||||
return ["error" => "File uploads are not allowed for this share."];
|
return ["error" => "File uploads are not allowed for this share."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate file upload presence.
|
// Validate file upload presence.
|
||||||
if ($fileUpload['error'] !== UPLOAD_ERR_OK) {
|
if ($fileUpload['error'] !== UPLOAD_ERR_OK) {
|
||||||
return ["error" => "File upload error. Code: " . $fileUpload['error']];
|
return ["error" => "File upload error. Code: " . $fileUpload['error']];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($fileUpload['size'] > $maxSize) {
|
if ($fileUpload['size'] > $maxSize) {
|
||||||
return ["error" => "File size exceeds allowed limit."];
|
return ["error" => "File size exceeds allowed limit."];
|
||||||
}
|
}
|
||||||
|
|
||||||
$uploadedName = basename($fileUpload['name']);
|
$uploadedName = basename($fileUpload['name']);
|
||||||
$ext = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION));
|
||||||
if (!in_array($ext, $allowedExtensions)) {
|
if (!in_array($ext, $allowedExtensions)) {
|
||||||
return ["error" => "File type not allowed."];
|
return ["error" => "File type not allowed."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the target folder from the share record.
|
// Determine the target folder from the share record.
|
||||||
$folderName = trim($record['folder'], "/\\");
|
$folderName = trim($record['folder'], "/\\");
|
||||||
$targetFolder = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
$targetFolder = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||||
if (!empty($folderName) && strtolower($folderName) !== 'root') {
|
if (!empty($folderName) && strtolower($folderName) !== 'root') {
|
||||||
$targetFolder .= $folderName;
|
$targetFolder .= $folderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify target folder exists.
|
// Verify target folder exists.
|
||||||
$realTargetFolder = realpath($targetFolder);
|
$realTargetFolder = realpath($targetFolder);
|
||||||
$uploadDirReal = realpath(UPLOAD_DIR);
|
$uploadDirReal = realpath(UPLOAD_DIR);
|
||||||
if ($realTargetFolder === false || strpos($realTargetFolder, $uploadDirReal) !== 0 || !is_dir($realTargetFolder)) {
|
if ($realTargetFolder === false || strpos($realTargetFolder, $uploadDirReal) !== 0 || !is_dir($realTargetFolder)) {
|
||||||
return ["error" => "Shared folder not found."];
|
return ["error" => "Shared folder not found."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new filename (using uniqid and sanitizing the original name).
|
// Generate a new filename (using uniqid and sanitizing the original name).
|
||||||
$newFilename = uniqid() . "_" . preg_replace('/[^A-Za-z0-9_\-\.]/', '_', $uploadedName);
|
$newFilename = uniqid() . "_" . preg_replace('/[^A-Za-z0-9_\-\.]/', '_', $uploadedName);
|
||||||
$targetPath = $realTargetFolder . DIRECTORY_SEPARATOR . $newFilename;
|
$targetPath = $realTargetFolder . DIRECTORY_SEPARATOR . $newFilename;
|
||||||
|
|
||||||
// Move the uploaded file.
|
// Move the uploaded file.
|
||||||
if (!move_uploaded_file($fileUpload['tmp_name'], $targetPath)) {
|
if (!move_uploaded_file($fileUpload['tmp_name'], $targetPath)) {
|
||||||
return ["error" => "Failed to move the uploaded file."];
|
return ["error" => "Failed to move the uploaded file."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Metadata Update ---
|
// --- Metadata Update ---
|
||||||
// Determine metadata file.
|
// Determine metadata file.
|
||||||
$metadataKey = (empty($folderName) || strtolower($folderName) === "root") ? "root" : $folderName;
|
$metadataKey = (empty($folderName) || strtolower($folderName) === "root") ? "root" : $folderName;
|
||||||
@@ -564,7 +567,32 @@ class FolderModel {
|
|||||||
"uploader" => $uploader
|
"uploader" => $uploader
|
||||||
];
|
];
|
||||||
file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT));
|
file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT));
|
||||||
|
|
||||||
return ["success" => "File uploaded successfully.", "newFilename" => $newFilename];
|
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
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
echo "🚀 Running start.sh..."
|
echo "🚀 Running start.sh..."
|
||||||
|
|
||||||
# Warn if default persistent tokens key is in use
|
# 1) Token‐key warning
|
||||||
if [ "$PERSISTENT_TOKENS_KEY" = "default_please_change_this_key" ]; then
|
if [ "${PERSISTENT_TOKENS_KEY}" = "default_please_change_this_key" ]; then
|
||||||
echo "⚠️ WARNING: Using default persistent tokens key. Please override PERSISTENT_TOKENS_KEY for production."
|
echo "⚠️ WARNING: Using default persistent tokens key—override for production."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update config.php based on environment variables
|
# 2) Update config.php based on environment variables
|
||||||
CONFIG_FILE="/var/www/config/config.php"
|
CONFIG_FILE="/var/www/config/config.php"
|
||||||
if [ -f "$CONFIG_FILE" ]; then
|
if [ -f "${CONFIG_FILE}" ]; then
|
||||||
echo "🔄 Updating config.php based on environment variables..."
|
echo "🔄 Updating config.php from env vars..."
|
||||||
if [ -n "$TIMEZONE" ]; then
|
[ -n "${TIMEZONE:-}" ] && sed -i "s|define('TIMEZONE',[[:space:]]*'[^']*');|define('TIMEZONE', '${TIMEZONE}');|" "${CONFIG_FILE}"
|
||||||
echo " Setting TIMEZONE to $TIMEZONE"
|
[ -n "${DATE_TIME_FORMAT:-}" ] && sed -i "s|define('DATE_TIME_FORMAT',[[:space:]]*'[^']*');|define('DATE_TIME_FORMAT', '${DATE_TIME_FORMAT}');|" "${CONFIG_FILE}"
|
||||||
sed -i "s|define('TIMEZONE',[[:space:]]*'[^']*');|define('TIMEZONE', '$TIMEZONE');|" "$CONFIG_FILE"
|
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||||
fi
|
sed -i "s|define('TOTAL_UPLOAD_SIZE',[[:space:]]*'[^']*');|define('TOTAL_UPLOAD_SIZE', '${TOTAL_UPLOAD_SIZE}');|" "${CONFIG_FILE}"
|
||||||
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"
|
|
||||||
fi
|
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
|
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
|
mkdir -p /etc/php/8.3/apache2/conf.d
|
||||||
|
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||||
# Update PHP upload limits at runtime if TOTAL_UPLOAD_SIZE is set.
|
echo "🔄 Setting PHP upload limits to ${TOTAL_UPLOAD_SIZE}"
|
||||||
if [ -n "$TOTAL_UPLOAD_SIZE" ]; then
|
cat > /etc/php/8.3/apache2/conf.d/99-custom.ini <<EOF
|
||||||
echo "🔄 Updating PHP upload limits with TOTAL_UPLOAD_SIZE=$TOTAL_UPLOAD_SIZE"
|
upload_max_filesize = ${TOTAL_UPLOAD_SIZE}
|
||||||
echo "upload_max_filesize = $TOTAL_UPLOAD_SIZE" > /etc/php/8.3/apache2/conf.d/99-custom.ini
|
post_max_size = ${TOTAL_UPLOAD_SIZE}
|
||||||
echo "post_max_size = $TOTAL_UPLOAD_SIZE" >> /etc/php/8.3/apache2/conf.d/99-custom.ini
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update Apache LimitRequestBody based on TOTAL_UPLOAD_SIZE if set.
|
# 4) Adjust Apache LimitRequestBody
|
||||||
if [ -n "$TOTAL_UPLOAD_SIZE" ]; then
|
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||||
size_str=$(echo "$TOTAL_UPLOAD_SIZE" | tr '[:upper:]' '[:lower:]')
|
# convert to bytes
|
||||||
factor=1
|
size_str=$(echo "${TOTAL_UPLOAD_SIZE}" | tr '[:upper:]' '[:lower:]')
|
||||||
case "${size_str: -1}" in
|
case "${size_str: -1}" in
|
||||||
g)
|
g) factor=$((1024*1024*1024)); num=${size_str%g} ;;
|
||||||
factor=$((1024*1024*1024))
|
m) factor=$((1024*1024)); num=${size_str%m} ;;
|
||||||
size_num=${size_str%g}
|
k) factor=1024; num=${size_str%k} ;;
|
||||||
;;
|
*) factor=1; num=${size_str} ;;
|
||||||
m)
|
|
||||||
factor=$((1024*1024))
|
|
||||||
size_num=${size_str%m}
|
|
||||||
;;
|
|
||||||
k)
|
|
||||||
factor=1024
|
|
||||||
size_num=${size_str%k}
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
size_num=$size_str
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
LIMIT_REQUEST_BODY=$((size_num * factor))
|
LIMIT_REQUEST_BODY=$(( num * factor ))
|
||||||
echo "🔄 Setting Apache LimitRequestBody to $LIMIT_REQUEST_BODY bytes (from TOTAL_UPLOAD_SIZE=$TOTAL_UPLOAD_SIZE)"
|
echo "🔄 Setting Apache LimitRequestBody to ${LIMIT_REQUEST_BODY} bytes"
|
||||||
cat <<EOF > /etc/apache2/conf-enabled/limit_request_body.conf
|
cat > /etc/apache2/conf-enabled/limit_request_body.conf <<EOF
|
||||||
<Directory "/var/www/public">
|
<Directory "/var/www/public">
|
||||||
LimitRequestBody $LIMIT_REQUEST_BODY
|
LimitRequestBody ${LIMIT_REQUEST_BODY}
|
||||||
</Directory>
|
</Directory>
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set Apache Timeout (default is 300 seconds)
|
# 5) Configure Apache timeout (600s)
|
||||||
echo "🔄 Setting Apache Timeout to 600 seconds"
|
cat > /etc/apache2/conf-enabled/timeout.conf <<EOF
|
||||||
cat <<EOF > /etc/apache2/conf-enabled/timeout.conf
|
|
||||||
Timeout 600
|
Timeout 600
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "🔥 Final Apache Timeout configuration:"
|
# 6) Override ports if provided
|
||||||
cat /etc/apache2/conf-enabled/timeout.conf
|
if [ -n "${HTTP_PORT:-}" ]; then
|
||||||
|
sed -i "s/^Listen 80$/Listen ${HTTP_PORT}/" /etc/apache2/ports.conf
|
||||||
# Update Apache ports if environment variables are provided
|
sed -i "s/<VirtualHost \*:80>/<VirtualHost *:${HTTP_PORT}>/" /etc/apache2/sites-available/000-default.conf
|
||||||
if [ -n "$HTTP_PORT" ]; then
|
fi
|
||||||
echo "🔄 Setting Apache HTTP port to $HTTP_PORT"
|
if [ -n "${HTTPS_PORT:-}" ]; then
|
||||||
sed -i "s/^Listen 80$/Listen $HTTP_PORT/" /etc/apache2/ports.conf
|
sed -i "s/^Listen 443$/Listen ${HTTPS_PORT}/" /etc/apache2/ports.conf
|
||||||
sed -i "s/<VirtualHost \*:80>/<VirtualHost *:$HTTP_PORT>/" /etc/apache2/sites-available/000-default.conf
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$HTTPS_PORT" ]; then
|
# 7) Set ServerName
|
||||||
echo "🔄 Setting Apache HTTPS port to $HTTPS_PORT"
|
if [ -n "${SERVER_NAME:-}" ]; then
|
||||||
sed -i "s/^Listen 443$/Listen $HTTPS_PORT/" /etc/apache2/ports.conf
|
echo "ServerName ${SERVER_NAME}" >> /etc/apache2/apache2.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
|
|
||||||
else
|
else
|
||||||
echo "🔄 Setting Apache ServerName to default: FileRise"
|
|
||||||
echo "ServerName FileRise" >> /etc/apache2/apache2.conf
|
echo "ServerName FileRise" >> /etc/apache2/apache2.conf
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Final /etc/apache2/ports.conf content:"
|
# 8) Prepare dynamic data directories with least privilege
|
||||||
cat /etc/apache2/ports.conf
|
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."
|
# 9) Initialize persistent files if absent
|
||||||
|
|
||||||
# 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)
|
|
||||||
if [ ! -f /var/www/users/users.txt ]; then
|
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
|
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
|
chmod 664 /var/www/users/users.txt
|
||||||
else
|
|
||||||
echo "ℹ️ users.txt already exists; preserving persistent data."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create createdTags.json only if it doesn't already exist (preserving persistent data)
|
|
||||||
if [ ! -f /var/www/metadata/createdTags.json ]; then
|
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
|
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
|
chmod 664 /var/www/metadata/createdTags.json
|
||||||
else
|
|
||||||
echo "ℹ️ createdTags.json already exists; preserving persistent data."
|
|
||||||
fi
|
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..."
|
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