Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d6a1be777 | ||
|
|
56f34ba362 | ||
|
|
4d329e046f | ||
|
|
f3977153fb | ||
|
|
274bedd186 | ||
|
|
2e4dbe7f7f | ||
|
|
0334e443eb | ||
|
|
76f5ed5c96 | ||
|
|
18f588dc24 | ||
|
|
491c686762 | ||
|
|
25303df677 | ||
|
|
ae0d63b86f | ||
|
|
41ade2e205 |
117
CHANGELOG.md
117
CHANGELOG.md
@@ -1,5 +1,116 @@
|
||||
# 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)**
|
||||
@@ -49,6 +160,8 @@
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -208,7 +321,7 @@
|
||||
Refactored to:
|
||||
1. Fetch CSRF
|
||||
2. POST credentials to `/api/auth/auth.php`
|
||||
3. On `totp_required`, re‑fetch CSRF *again* before calling `openTOTPLoginModal()`
|
||||
3. On `totp_required`, re‑fetch CSRF again before calling `openTOTPLoginModal()`
|
||||
4. Handle full logins vs. TOTP flows cleanly.
|
||||
|
||||
- **TOTP handlers update**
|
||||
@@ -1154,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.
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -120,6 +120,10 @@ RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
|
||||
<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
|
||||
@@ -127,7 +131,7 @@ RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
|
||||
EOF
|
||||
|
||||
# Enable required modules
|
||||
RUN a2enmod rewrite headers proxy proxy_fcgi expires deflate
|
||||
RUN a2enmod rewrite headers proxy proxy_fcgi expires deflate ssl
|
||||
|
||||
EXPOSE 80 443
|
||||
COPY start.sh /usr/local/bin/start.sh
|
||||
|
||||
@@ -108,16 +108,16 @@ FileRise will be accessible at `http://localhost:8080` (or your server’s IP).
|
||||
|
||||
If you prefer to run FileRise on a traditional web server (LAMP stack or similar):
|
||||
|
||||
- **Requirements:** PHP 8.1 or higher, Apache (with mod_php) or another web server configured for PHP. Ensure PHP extensions json, curl, and zip are enabled. No database needed.
|
||||
- **Requirements:** PHP 8.3 or higher, Apache (with mod_php) or another web server configured for PHP. Ensure PHP extensions json, curl, and zip are enabled. No database needed.
|
||||
- **Download Files:** Clone this repo or download the [latest release archive](https://github.com/error311/FileRise/releases).
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/error311/FileRise.git
|
||||
```
|
||||
|
||||
Place the files into your web server’s directory (e.g., `/var/www/public`). 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.)
|
||||
- **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):
|
||||
|
||||
|
||||
@@ -19,17 +19,13 @@ if (isset($_GET['spec'])) {
|
||||
<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"
|
||||
<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>
|
||||
<script>
|
||||
if (!customElements.get('redoc')) {
|
||||
Redoc.init('api.php?spec=1', {}, document.getElementById('redoc-container'));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/addUser.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->addUser();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/admin/getConfig.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/adminController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||
|
||||
$adminController = new AdminController();
|
||||
$adminController->getConfig();
|
||||
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
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/adminController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||
|
||||
$adminController = new AdminController();
|
||||
$adminController->updateConfig();
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->auth();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/auth/checkAuth.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->checkAuth();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/auth/login_basic.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->loginBasic();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/auth/logout.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->logout();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/auth/token.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->getToken();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/changePassword.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->changePassword();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/copyFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->copyFiles();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/createShareLink.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->createShareLink();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/deleteFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->deleteFiles();
|
||||
6
public/api/file/deleteShareLink.php
Normal file
6
public/api/file/deleteShareLink.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->deleteShareLink();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/deleteTrashFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->deleteTrashFiles();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/download.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->downloadFile();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/downloadZip.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->downloadZip();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/extractZip.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->extractZip();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/getFileList.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getFileList();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/getFileTag.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getFileTags();
|
||||
6
public/api/file/getShareLinks.php
Normal file
6
public/api/file/getShareLinks.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getShareLinks();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/getTrashItems.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getTrashItems();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/moveFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->moveFiles();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/renameFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->renameFile();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/restoreFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->restoreFiles();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/saveFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->saveFile();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/saveFileTag.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->saveFileTag();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/file/share.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->shareFile();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/createFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->createFolder();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/createShareFolderLink.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->createShareFolderLink();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/deleteFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->deleteFolder();
|
||||
6
public/api/folder/deleteShareFolderLink.php
Normal file
6
public/api/folder/deleteShareFolderLink.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->deleteShareFolderLink();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/downloadSharedFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->downloadSharedFile();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/getFolderList.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->getFolderList();
|
||||
6
public/api/folder/getShareFolderLinks.php
Normal file
6
public/api/folder/getShareFolderLinks.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->getShareFolderLinks();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/renameFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->renameFolder();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/shareFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->shareFolder();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/folder/uploadToSharedFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->uploadToSharedFolder();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/getUserPermissions.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->getUserPermissions();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/getUsers.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->getUsers(); // This will output the JSON response
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/removeUser.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->removeUser();
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->disableTOTP();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/totp_recover.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->recoverTOTP();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/totp_saveCode.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->saveTOTPRecoveryCode();
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->setupTOTP();
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->verifyTOTP();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/updateUserPanel.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->updateUserPanel();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/updateUserPermissions.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->updateUserPermissions();
|
||||
@@ -2,7 +2,7 @@
|
||||
// public/api/upload/removeChunks.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/uploadController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UploadController.php';
|
||||
|
||||
$uploadController = new UploadController();
|
||||
$uploadController->removeChunks();
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
// public/api/upload/upload.php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/uploadController.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UploadController.php';
|
||||
|
||||
$uploadController = new UploadController();
|
||||
$uploadController->handleUpload();
|
||||
@@ -182,7 +182,7 @@
|
||||
<form id="authForm" method="post">
|
||||
<div class="form-group">
|
||||
<label for="loginUsername" data-i18n-key="user">User:</label>
|
||||
<input type="text" class="form-control" id="loginUsername" name="username" required />
|
||||
<input type="text" class="form-control" id="loginUsername" name="username" required autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword" data-i18n-key="password">Password:</label>
|
||||
@@ -442,18 +442,30 @@
|
||||
<div id="addUserModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3 data-i18n-key="create_new_user_title">Create New User</h3>
|
||||
<label for="newUsername" data-i18n-key="username">Username:</label>
|
||||
<input type="text" id="newUsername" class="form-control" />
|
||||
<label for="addUserPassword" data-i18n-key="password">Password:</label>
|
||||
<input type="password" id="addUserPassword" class="form-control" />
|
||||
<div id="adminCheckboxContainer">
|
||||
<input type="checkbox" id="isAdmin" />
|
||||
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
|
||||
<button id="saveUserBtn" class="btn btn-primary" data-i18n-key="save_user">Save User</button>
|
||||
</div>
|
||||
<!-- 1) Add a form around these fields -->
|
||||
<form id="addUserForm">
|
||||
<label for="newUsername" data-i18n-key="username">Username:</label>
|
||||
<input type="text" id="newUsername" class="form-control" required />
|
||||
|
||||
<label for="addUserPassword" data-i18n-key="password">Password:</label>
|
||||
<input type="password" id="addUserPassword" class="form-control" required />
|
||||
|
||||
<div id="adminCheckboxContainer">
|
||||
<input type="checkbox" id="isAdmin" />
|
||||
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<!-- Cancel stays type="button" -->
|
||||
<button type="button" id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<!-- Save becomes type="submit" -->
|
||||
<button type="submit" id="saveUserBtn" class="btn btn-primary" data-i18n-key="save_user">
|
||||
Save User
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="removeUserModal" class="modal">
|
||||
|
||||
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,
|
||||
openTOTPModal,
|
||||
closeTOTPModal,
|
||||
openAdminPanel,
|
||||
closeAdminPanel,
|
||||
setLastLoginData
|
||||
} from './authModals.js';
|
||||
import { openAdminPanel } from './adminPanel.js';
|
||||
|
||||
// Production OIDC configuration (override via API as needed)
|
||||
const currentOIDCConfig = {
|
||||
@@ -125,10 +124,17 @@ function updateItemsPerPageSelect() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
|
||||
const authForm = document.getElementById("authForm");
|
||||
|
||||
if (authForm) authForm.style.display = disableFormLogin ? "none" : "block";
|
||||
if
|
||||
(authForm) {
|
||||
authForm.style.display = disableFormLogin ? "none" : "block";
|
||||
setTimeout(() => {
|
||||
const loginInput = document.getElementById('loginUsername');
|
||||
if (loginInput) loginInput.focus();
|
||||
}, 0);
|
||||
}
|
||||
const basicAuthLink = document.querySelector("a[href='/api/auth/login_basic.php']");
|
||||
if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
|
||||
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||
@@ -187,7 +193,7 @@ function updateAuthenticatedUI(data) {
|
||||
toggleVisibility("mainOperations", true);
|
||||
toggleVisibility("uploadFileForm", true);
|
||||
toggleVisibility("fileListContainer", true);
|
||||
attachEnterKeyListener("addUserModal", "saveUserBtn");
|
||||
//attachEnterKeyListener("addUserModal", "saveUserBtn");
|
||||
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
|
||||
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||
@@ -443,34 +449,46 @@ function initAuth() {
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById("newUsername").focus();
|
||||
});
|
||||
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
||||
const newUsername = document.getElementById("newUsername").value.trim();
|
||||
const newPassword = document.getElementById("addUserPassword").value.trim();
|
||||
const isAdmin = document.getElementById("isAdmin").checked;
|
||||
if (!newUsername || !newPassword) {
|
||||
showToast("Username and password are required!");
|
||||
return;
|
||||
}
|
||||
let url = "/api/addUser.php";
|
||||
if (window.setupMode) url += "?setup=1";
|
||||
fetchWithCsrf(url, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||
|
||||
// remove your old saveUserBtn click-handler…
|
||||
|
||||
// instead:
|
||||
const addUserForm = document.getElementById("addUserForm");
|
||||
addUserForm.addEventListener("submit", function (e) {
|
||||
e.preventDefault(); // stop the browser from reloading the page
|
||||
|
||||
const newUsername = document.getElementById("newUsername").value.trim();
|
||||
const newPassword = document.getElementById("addUserPassword").value.trim();
|
||||
const isAdmin = document.getElementById("isAdmin").checked;
|
||||
|
||||
if (!newUsername || !newPassword) {
|
||||
showToast("Username and password are required!");
|
||||
return;
|
||||
}
|
||||
|
||||
let url = "/api/addUser.php";
|
||||
if (window.setupMode) url += "?setup=1";
|
||||
|
||||
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())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("User added successfully!");
|
||||
closeAddUserModal();
|
||||
checkAuthentication(false);
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not add user"));
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
});
|
||||
.catch(() => {
|
||||
showToast("Error: Could not add user");
|
||||
});
|
||||
});
|
||||
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
|
||||
|
||||
document.getElementById("removeUserBtn").addEventListener("click", function () {
|
||||
|
||||
@@ -3,8 +3,6 @@ import { sendRequest } from './networkUtils.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
import { loadAdminConfigFunc } from './auth.js';
|
||||
|
||||
const version = "v1.2.6"; // Update this version string as needed
|
||||
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||
|
||||
let lastLoginData = null;
|
||||
export function setLastLoginData(data) {
|
||||
@@ -544,539 +542,4 @@ export function closeTOTPModal(disable = true) {
|
||||
})
|
||||
.catch(() => { showToast(t("error_disabling_totp_setting")); });
|
||||
}
|
||||
}
|
||||
|
||||
// Global variable to hold the initial state of the admin form.
|
||||
let originalAdminConfig = {};
|
||||
|
||||
// Capture the initial state of the admin form fields.
|
||||
function captureInitialAdminConfig() {
|
||||
originalAdminConfig = {
|
||||
headerTitle: document.getElementById("headerTitle").value.trim(),
|
||||
oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||
oidcClientId: document.getElementById("oidcClientId").value.trim(),
|
||||
oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
||||
oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(),
|
||||
disableFormLogin: document.getElementById("disableFormLogin").checked,
|
||||
disableBasicAuth: document.getElementById("disableBasicAuth").checked,
|
||||
disableOIDCLogin: document.getElementById("disableOIDCLogin").checked,
|
||||
globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim()
|
||||
};
|
||||
}
|
||||
|
||||
// Compare current values to the captured initial state.
|
||||
function hasUnsavedChanges() {
|
||||
return (
|
||||
document.getElementById("headerTitle").value.trim() !== originalAdminConfig.headerTitle ||
|
||||
document.getElementById("oidcProviderUrl").value.trim() !== originalAdminConfig.oidcProviderUrl ||
|
||||
document.getElementById("oidcClientId").value.trim() !== originalAdminConfig.oidcClientId ||
|
||||
document.getElementById("oidcClientSecret").value.trim() !== originalAdminConfig.oidcClientSecret ||
|
||||
document.getElementById("oidcRedirectUri").value.trim() !== originalAdminConfig.oidcRedirectUri ||
|
||||
document.getElementById("disableFormLogin").checked !== originalAdminConfig.disableFormLogin ||
|
||||
document.getElementById("disableBasicAuth").checked !== originalAdminConfig.disableBasicAuth ||
|
||||
document.getElementById("disableOIDCLogin").checked !== originalAdminConfig.disableOIDCLogin ||
|
||||
document.getElementById("globalOtpauthUrl").value.trim() !== originalAdminConfig.globalOtpauthUrl
|
||||
);
|
||||
}
|
||||
|
||||
// Use your custom confirmation modal.
|
||||
function showCustomConfirmModal(message) {
|
||||
return new Promise((resolve) => {
|
||||
// Get modal elements from DOM.
|
||||
const modal = document.getElementById("customConfirmModal");
|
||||
const messageElem = document.getElementById("confirmMessage");
|
||||
const yesBtn = document.getElementById("confirmYesBtn");
|
||||
const noBtn = document.getElementById("confirmNoBtn");
|
||||
|
||||
// Set the message in the modal.
|
||||
messageElem.textContent = message;
|
||||
modal.style.display = "block";
|
||||
|
||||
// Define event handlers.
|
||||
function onYes() {
|
||||
cleanup();
|
||||
resolve(true);
|
||||
}
|
||||
function onNo() {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}
|
||||
// Remove event listeners and hide modal after choice.
|
||||
function cleanup() {
|
||||
yesBtn.removeEventListener("click", onYes);
|
||||
noBtn.removeEventListener("click", onNo);
|
||||
modal.style.display = "none";
|
||||
}
|
||||
|
||||
yesBtn.addEventListener("click", onYes);
|
||||
noBtn.addEventListener("click", onNo);
|
||||
});
|
||||
}
|
||||
|
||||
export function openAdminPanel() {
|
||||
fetch("/api/admin/getConfig.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
if (config.header_title) {
|
||||
document.querySelector(".header-title h1").textContent = config.header_title;
|
||||
window.headerTitle = config.header_title || "FileRise";
|
||||
}
|
||||
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
||||
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
||||
|
||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||
const modalContentStyles = `
|
||||
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
||||
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
max-height: 90vh;
|
||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
||||
`;
|
||||
|
||||
let adminModal = document.getElementById("adminPanelModal");
|
||||
|
||||
if (!adminModal) {
|
||||
adminModal = document.createElement("div");
|
||||
adminModal.id = "adminPanelModal";
|
||||
adminModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: ${overlayBackground};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3000;
|
||||
`;
|
||||
adminModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeAdminPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>${adminTitle}</h3>
|
||||
<form id="adminPanelForm">
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>${t("user_management")}</legend>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button type="button" id="adminOpenAddUser" class="btn btn-success">${t("add_user")}</button>
|
||||
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">${t("remove_user")}</button>
|
||||
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${t("user_permissions")}</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>Header Settings</legend>
|
||||
<div class="form-group">
|
||||
<label for="headerTitle">Header Title:</label>
|
||||
<input type="text" id="headerTitle" class="form-control" value="${window.headerTitle}" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>${t("login_options")}</legend>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableFormLogin" />
|
||||
<label for="disableFormLogin">${t("disable_login_form")}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableBasicAuth" />
|
||||
<label for="disableBasicAuth">${t("disable_basic_http_auth")}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableOIDCLogin" />
|
||||
<label for="disableOIDCLogin">${t("disable_oidc_login")}</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- New WebDAV setting -->
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>WebDAV Access</legend>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="enableWebDAV" />
|
||||
<label for="enableWebDAV">Enable WebDAV</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<!-- End WebDAV setting -->
|
||||
|
||||
<!-- New Shared Max Upload Size setting -->
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>Shared Max Upload Size (bytes)</legend>
|
||||
<div class="form-group">
|
||||
<input type="number" id="sharedMaxUploadSize" class="form-control"
|
||||
placeholder="e.g. 52428800" />
|
||||
<small>Enter maximum bytes allowed for shared-folder uploads</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
<!-- End Shared Max Upload Size setting -->
|
||||
|
||||
<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
|
||||
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();
|
||||
});
|
||||
|
||||
// Save handler
|
||||
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||
const enableWebDAVCheckbox = document.getElementById("enableWebDAV");
|
||||
const sharedMaxUploadSizeInput = document.getElementById("sharedMaxUploadSize");
|
||||
|
||||
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 enableWebDAV = enableWebDAVCheckbox.checked;
|
||||
const sharedMaxUploadSize = parseInt(sharedMaxUploadSizeInput.value, 10) || 0;
|
||||
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
||||
|
||||
sendRequest("/api/admin/updateConfig.php", "POST", {
|
||||
header_title: newHeaderTitle,
|
||||
oidc: newOIDCConfig,
|
||||
disableFormLogin,
|
||||
disableBasicAuth,
|
||||
disableOIDCLogin,
|
||||
enableWebDAV,
|
||||
sharedMaxUploadSize,
|
||||
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);
|
||||
localStorage.setItem("enableWebDAV", enableWebDAV);
|
||||
localStorage.setItem("sharedMaxUploadSize", sharedMaxUploadSize);
|
||||
if (typeof window.updateLoginOptionsUI === "function") {
|
||||
window.updateLoginOptionsUI({
|
||||
disableFormLogin,
|
||||
disableBasicAuth,
|
||||
disableOIDCLogin
|
||||
});
|
||||
}
|
||||
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); });
|
||||
|
||||
// Initial checkbox and input states
|
||||
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 {
|
||||
// Update existing modal and show
|
||||
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;
|
||||
document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
|
||||
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
||||
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";
|
||||
document.getElementById("enableWebDAV").checked = localStorage.getItem("enableWebDAV") === "true";
|
||||
document.getElementById("sharedMaxUploadSize").value = localStorage.getItem("sharedMaxUploadSize") || "";
|
||||
adminModal.style.display = "flex";
|
||||
captureInitialAdminConfig();
|
||||
} else {
|
||||
openAdminPanel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function closeAdminPanel() {
|
||||
if (hasUnsavedChanges()) {
|
||||
const userConfirmed = await showCustomConfirmModal(t("unsaved_changes_confirm"));
|
||||
if (!userConfirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const adminModal = document.getElementById("adminPanelModal");
|
||||
if (adminModal) adminModal.style.display = "none";
|
||||
}
|
||||
|
||||
// --- New: User Permissions Modal ---
|
||||
export function openUserPermissionsModal() {
|
||||
let userPermissionsModal = document.getElementById("userPermissionsModal");
|
||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||
const modalContentStyles = `
|
||||
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
||||
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
||||
padding: 20px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
if (!userPermissionsModal) {
|
||||
userPermissionsModal = document.createElement("div");
|
||||
userPermissionsModal.id = "userPermissionsModal";
|
||||
userPermissionsModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: ${overlayBackground};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3500;
|
||||
`;
|
||||
userPermissionsModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeUserPermissionsModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>${t("user_permissions")}</h3>
|
||||
<div id="userPermissionsList" style="max-height: 300px; overflow-y: auto; margin-bottom: 15px;">
|
||||
<!-- User rows will be loaded here -->
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
||||
<button type="button" id="cancelUserPermissionsBtn" class="btn btn-secondary">${t("cancel")}</button>
|
||||
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">${t("save_permissions")}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(userPermissionsModal);
|
||||
document.getElementById("closeUserPermissionsModal").addEventListener("click", () => {
|
||||
userPermissionsModal.style.display = "none";
|
||||
});
|
||||
document.getElementById("cancelUserPermissionsBtn").addEventListener("click", () => {
|
||||
userPermissionsModal.style.display = "none";
|
||||
});
|
||||
document.getElementById("saveUserPermissionsBtn").addEventListener("click", () => {
|
||||
// Collect permissions data from each user row.
|
||||
const rows = userPermissionsModal.querySelectorAll(".user-permission-row");
|
||||
const permissionsData = [];
|
||||
rows.forEach(row => {
|
||||
const username = row.getAttribute("data-username");
|
||||
const folderOnlyCheckbox = row.querySelector("input[data-permission='folderOnly']");
|
||||
const readOnlyCheckbox = row.querySelector("input[data-permission='readOnly']");
|
||||
const disableUploadCheckbox = row.querySelector("input[data-permission='disableUpload']");
|
||||
permissionsData.push({
|
||||
username,
|
||||
folderOnly: folderOnlyCheckbox.checked,
|
||||
readOnly: readOnlyCheckbox.checked,
|
||||
disableUpload: disableUploadCheckbox.checked
|
||||
});
|
||||
});
|
||||
// Send the permissionsData to the server.
|
||||
sendRequest("/api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
showToast(t("user_permissions_updated_successfully"));
|
||||
userPermissionsModal.style.display = "none";
|
||||
} else {
|
||||
showToast(t("error_updating_permissions") + ": " + (response.error || t("unknown_error")));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast(t("error_updating_permissions"));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
userPermissionsModal.style.display = "flex";
|
||||
}
|
||||
// Load the list of users into the modal.
|
||||
loadUserPermissionsList();
|
||||
}
|
||||
|
||||
function loadUserPermissionsList() {
|
||||
const listContainer = document.getElementById("userPermissionsList");
|
||||
if (!listContainer) return;
|
||||
listContainer.innerHTML = "";
|
||||
|
||||
// First, fetch the current permissions from the server.
|
||||
fetch("/api/getUserPermissions.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(permissionsData => {
|
||||
// Then, fetch the list of users.
|
||||
return fetch("/api/getUsers.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(usersData => {
|
||||
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
|
||||
if (users.length === 0) {
|
||||
listContainer.innerHTML = "<p>" + t("no_users_found") + "</p>";
|
||||
return;
|
||||
}
|
||||
users.forEach(user => {
|
||||
// Skip admin users.
|
||||
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
|
||||
|
||||
// Use stored permissions if available; otherwise fall back to defaults.
|
||||
const defaultPerm = {
|
||||
folderOnly: false,
|
||||
readOnly: false,
|
||||
disableUpload: false,
|
||||
};
|
||||
|
||||
// Normalize the username key to match server storage (e.g., lowercase)
|
||||
const usernameKey = user.username.toLowerCase();
|
||||
|
||||
const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData))
|
||||
? permissionsData[usernameKey]
|
||||
: defaultPerm;
|
||||
|
||||
// Create a row for the user.
|
||||
const row = document.createElement("div");
|
||||
row.classList.add("user-permission-row");
|
||||
row.setAttribute("data-username", user.username);
|
||||
row.style.padding = "10px 0";
|
||||
row.innerHTML = `
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">${user.username}</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="folderOnly" ${userPerm.folderOnly ? "checked" : ""} />
|
||||
${t("user_folder_only")}
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="readOnly" ${userPerm.readOnly ? "checked" : ""} />
|
||||
${t("read_only")}
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="disableUpload" ${userPerm.disableUpload ? "checked" : ""} />
|
||||
${t("disable_upload")}
|
||||
</label>
|
||||
</div>
|
||||
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
|
||||
`;
|
||||
listContainer.appendChild(row);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";
|
||||
});
|
||||
}
|
||||
@@ -25,8 +25,9 @@ export function toggleAllCheckboxes(masterCheckbox) {
|
||||
const checkboxes = document.querySelectorAll(".file-checkbox");
|
||||
checkboxes.forEach(chk => {
|
||||
chk.checked = masterCheckbox.checked;
|
||||
updateRowHighlight(chk);
|
||||
});
|
||||
updateFileActionButtons(); // update buttons based on current selection
|
||||
updateFileActionButtons();
|
||||
}
|
||||
|
||||
export function updateFileActionButtons() {
|
||||
@@ -38,6 +39,21 @@ export function updateFileActionButtons() {
|
||||
const zipBtn = document.getElementById("downloadZipBtn");
|
||||
const extractZipBtn = document.getElementById("extractZipBtn");
|
||||
|
||||
// keep the “select all” in sync ——
|
||||
const master = document.getElementById("selectAll");
|
||||
if (master) {
|
||||
if (selectedCheckboxes.length === fileCheckboxes.length) {
|
||||
master.checked = true;
|
||||
master.indeterminate = false;
|
||||
} else if (selectedCheckboxes.length === 0) {
|
||||
master.checked = false;
|
||||
master.indeterminate = false;
|
||||
} else {
|
||||
master.checked = false;
|
||||
master.indeterminate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (fileCheckboxes.length === 0) {
|
||||
if (copyBtn) copyBtn.style.display = "none";
|
||||
if (moveBtn) moveBtn.style.display = "none";
|
||||
@@ -271,8 +287,6 @@ export function toggleRowSelection(event, fileName) {
|
||||
const start = Math.min(currentIndex, lastIndex);
|
||||
const end = Math.max(currentIndex, lastIndex);
|
||||
|
||||
// If neither CTRL nor Meta is pressed, you might choose
|
||||
// to clear existing selections. For this example we leave existing selections intact.
|
||||
for (let i = start; i <= end; i++) {
|
||||
const cb = allRows[i].querySelector(".file-checkbox");
|
||||
if (cb) {
|
||||
|
||||
@@ -340,47 +340,87 @@ export function renderFileTable(folder, container) {
|
||||
|
||||
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||
|
||||
// pagination clicks
|
||||
const prevBtn = document.getElementById("prevPageBtn");
|
||||
if (prevBtn) prevBtn.addEventListener("click", () => {
|
||||
if (window.currentPage > 1) {
|
||||
window.currentPage--;
|
||||
renderFileTable(folder, container);
|
||||
}
|
||||
});
|
||||
const nextBtn = document.getElementById("nextPageBtn");
|
||||
if (nextBtn) nextBtn.addEventListener("click", () => {
|
||||
// totalPages is computed above in this scope
|
||||
if (window.currentPage < totalPages) {
|
||||
window.currentPage++;
|
||||
renderFileTable(folder, container);
|
||||
}
|
||||
});
|
||||
|
||||
// ADD: advanced search toggle
|
||||
const advToggle = document.getElementById("advancedSearchToggle");
|
||||
if (advToggle) advToggle.addEventListener("click", () => {
|
||||
toggleAdvancedSearch();
|
||||
});
|
||||
|
||||
// items-per-page selector
|
||||
const itemsSelect = document.getElementById("itemsPerPageSelect");
|
||||
if (itemsSelect) itemsSelect.addEventListener("change", e => {
|
||||
window.itemsPerPage = parseInt(e.target.value, 10);
|
||||
localStorage.setItem("itemsPerPage", window.itemsPerPage);
|
||||
window.currentPage = 1;
|
||||
renderFileTable(folder, container);
|
||||
});
|
||||
|
||||
// hook up the master checkbox
|
||||
const selectAll = document.getElementById("selectAll");
|
||||
if (selectAll) {
|
||||
selectAll.addEventListener("change", () => {
|
||||
toggleAllCheckboxes(selectAll);
|
||||
});
|
||||
}
|
||||
|
||||
// 1) Row-click selects the row
|
||||
fileListContent.querySelectorAll("tbody tr").forEach(row => {
|
||||
row.addEventListener("click", e => {
|
||||
// grab the underlying checkbox value
|
||||
const cb = row.querySelector(".file-checkbox");
|
||||
if (!cb) return;
|
||||
toggleRowSelection(e, cb.value);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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();
|
||||
|
||||
@@ -575,7 +615,7 @@ export function renderGalleryView(folder, container) {
|
||||
style="position:absolute; top:5px; left:5px; width:16px; height:16px;"></label>
|
||||
|
||||
<div class="gallery-preview" style="cursor:pointer;"
|
||||
data-preview-url="${folderPath+encodeURIComponent(file.name)}?t=${Date.now()}"
|
||||
data-preview-url="${folderPath + encodeURIComponent(file.name)}?t=${Date.now()}"
|
||||
data-preview-name="${file.name}">
|
||||
${thumbnail}
|
||||
</div>
|
||||
@@ -590,20 +630,20 @@ export function renderGalleryView(folder, container) {
|
||||
<div class="button-wrap" style="display:flex; justify-content:center; gap:5px; margin-top:5px;">
|
||||
<button type="button" class="btn btn-sm btn-success download-btn"
|
||||
data-download-name="${escapeHTML(file.name)}"
|
||||
data-download-folder="${file.folder||"root"}"
|
||||
data-download-folder="${file.folder || "root"}"
|
||||
title="${t('download')}">
|
||||
<i class="material-icons">file_download</i>
|
||||
</button>
|
||||
${file.editable ? `
|
||||
<button type="button" class="btn btn-sm edit-btn"
|
||||
data-edit-name="${escapeHTML(file.name)}"
|
||||
data-edit-folder="${file.folder||"root"}"
|
||||
data-edit-folder="${file.folder || "root"}"
|
||||
title="${t('edit')}">
|
||||
<i class="material-icons">edit</i>
|
||||
</button>` : ""}
|
||||
<button type="button" class="btn btn-sm btn-warning rename-btn"
|
||||
data-rename-name="${escapeHTML(file.name)}"
|
||||
data-rename-folder="${file.folder||"root"}"
|
||||
data-rename-folder="${file.folder || "root"}"
|
||||
title="${t('rename')}">
|
||||
<i class="material-icons">drive_file_rename_outline</i>
|
||||
</button>
|
||||
@@ -629,6 +669,40 @@ export function renderGalleryView(folder, container) {
|
||||
|
||||
// --- Now wire up all behaviors without inline handlers ---
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -4,36 +4,68 @@ import { fileData } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
export function openShareModal(file, folder) {
|
||||
// Remove any existing modal
|
||||
const existing = document.getElementById("shareModal");
|
||||
if (existing) existing.remove();
|
||||
|
||||
// Build the modal
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "shareModal";
|
||||
modal.classList.add("modal");
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content share-modal-content" style="width: 600px; max-width:90vw;">
|
||||
<div class="modal-content share-modal-content" style="width:600px;max-width:90vw;">
|
||||
<div class="modal-header">
|
||||
<h3>${t("share_file")}: ${escapeHTML(file.name)}</h3>
|
||||
<span class="close-image-modal" id="closeShareModal" title="Close">×</span>
|
||||
<span id="closeShareModal" title="${t("close")}" class="close-image-modal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>${t("set_expiration")}</p>
|
||||
<select id="shareExpiration">
|
||||
<option value="30">30 minutes</option>
|
||||
<option value="60" selected>60 minutes</option>
|
||||
<option value="120">120 minutes</option>
|
||||
<option value="180">180 minutes</option>
|
||||
<option value="240">240 minutes</option>
|
||||
<option value="1440">1 Day</option>
|
||||
<select id="shareExpiration" style="width:100%;padding:5px;">
|
||||
<option value="30">30 ${t("minutes")}</option>
|
||||
<option value="60" selected>60 ${t("minutes")}</option>
|
||||
<option value="120">120 ${t("minutes")}</option>
|
||||
<option value="180">180 ${t("minutes")}</option>
|
||||
<option value="240">240 ${t("minutes")}</option>
|
||||
<option value="1440">1 ${t("day")}</option>
|
||||
<option value="custom">${t("custom")}…</option>
|
||||
</select>
|
||||
<p>${t("password_optional")}</p>
|
||||
<input type="text" id="sharePassword" placeholder=${t("password_optional")} style="width: 100%;"/>
|
||||
<br>
|
||||
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">${t("generate_share_link")}</button>
|
||||
<div id="shareLinkDisplay" style="margin-top: 10px; display:none;">
|
||||
|
||||
<div id="customExpirationContainer" style="display:none;margin-top:10px;">
|
||||
<label for="customExpirationValue">${t("duration")}:</label>
|
||||
<input type="number" id="customExpirationValue" min="1" value="1" style="width:60px;margin:0 8px;"/>
|
||||
<select id="customExpirationUnit">
|
||||
<option value="seconds">${t("seconds")}</option>
|
||||
<option value="minutes" selected>${t("minutes")}</option>
|
||||
<option value="hours">${t("hours")}</option>
|
||||
<option value="days">${t("days")}</option>
|
||||
</select>
|
||||
<p class="share-warning" style="color:#a33;font-size:0.9em;margin-top:5px;">
|
||||
${t("custom_duration_warning")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="margin-top:15px;">${t("password_optional")}</p>
|
||||
<input
|
||||
type="text"
|
||||
id="sharePassword"
|
||||
placeholder="${t("password_optional")}"
|
||||
style="width:100%;padding:5px;"
|
||||
/>
|
||||
|
||||
<button
|
||||
id="generateShareLinkBtn"
|
||||
class="btn btn-primary"
|
||||
style="margin-top:15px;"
|
||||
>
|
||||
${t("generate_share_link")}
|
||||
</button>
|
||||
|
||||
<div id="shareLinkDisplay" style="margin-top:15px;display:none;">
|
||||
<p>${t("shareable_link")}</p>
|
||||
<input type="text" id="shareLinkInput" readonly style="width:100%;"/>
|
||||
<button id="copyShareLinkBtn" class="btn btn-primary" style="margin-top:5px;">${t("copy_link")}</button>
|
||||
<input type="text" id="shareLinkInput" readonly style="width:100%;padding:5px;"/>
|
||||
<button id="copyShareLinkBtn" class="btn btn-secondary" style="margin-top:5px;">
|
||||
${t("copy_link")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,52 +73,72 @@ export function openShareModal(file, folder) {
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = "block";
|
||||
|
||||
document.getElementById("closeShareModal").addEventListener("click", () => {
|
||||
modal.remove();
|
||||
});
|
||||
// Close handler
|
||||
document.getElementById("closeShareModal")
|
||||
.addEventListener("click", () => modal.remove());
|
||||
|
||||
document.getElementById("generateShareLinkBtn").addEventListener("click", () => {
|
||||
const expiration = document.getElementById("shareExpiration").value;
|
||||
const password = document.getElementById("sharePassword").value;
|
||||
fetch("/api/file/createShareLink.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder: folder,
|
||||
file: file.name,
|
||||
expirationMinutes: parseInt(expiration),
|
||||
password: password
|
||||
// Show/hide custom-duration inputs
|
||||
document.getElementById("shareExpiration")
|
||||
.addEventListener("change", e => {
|
||||
const container = document.getElementById("customExpirationContainer");
|
||||
container.style.display = e.target.value === "custom" ? "block" : "none";
|
||||
});
|
||||
|
||||
// Generate share link
|
||||
document.getElementById("generateShareLinkBtn")
|
||||
.addEventListener("click", () => {
|
||||
const sel = document.getElementById("shareExpiration");
|
||||
let value, unit;
|
||||
|
||||
if (sel.value === "custom") {
|
||||
value = parseInt(document.getElementById("customExpirationValue").value, 10);
|
||||
unit = document.getElementById("customExpirationUnit").value;
|
||||
} else {
|
||||
value = parseInt(sel.value, 10);
|
||||
unit = "minutes";
|
||||
}
|
||||
|
||||
const password = document.getElementById("sharePassword").value;
|
||||
|
||||
fetch("/api/file/createShareLink.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder,
|
||||
file: file.name,
|
||||
expirationValue: value,
|
||||
expirationUnit: unit,
|
||||
password
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.token) {
|
||||
const shareEndpoint = `${window.location.origin}/api/file/share.php`;
|
||||
const shareUrl = `${shareEndpoint}?token=${encodeURIComponent(data.token)}`;
|
||||
const displayDiv = document.getElementById("shareLinkDisplay");
|
||||
const inputField = document.getElementById("shareLinkInput");
|
||||
inputField.value = shareUrl;
|
||||
displayDiv.style.display = "block";
|
||||
const url = `${window.location.origin}/api/file/share.php?token=${encodeURIComponent(data.token)}`;
|
||||
document.getElementById("shareLinkInput").value = url;
|
||||
document.getElementById("shareLinkDisplay").style.display = "block";
|
||||
} else {
|
||||
showToast("Error generating share link: " + (data.error || "Unknown error"));
|
||||
showToast(t("error_generating_share") + ": " + (data.error||"Unknown"));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error generating share link:", err);
|
||||
showToast("Error generating share link.");
|
||||
console.error(err);
|
||||
showToast(t("error_generating_share"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("copyShareLinkBtn").addEventListener("click", () => {
|
||||
const input = document.getElementById("shareLinkInput");
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
showToast("Link copied to clipboard!");
|
||||
});
|
||||
// Copy to clipboard
|
||||
document.getElementById("copyShareLinkBtn")
|
||||
.addEventListener("click", () => {
|
||||
const input = document.getElementById("shareLinkInput");
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
showToast(t("link_copied"));
|
||||
});
|
||||
}
|
||||
|
||||
export function previewFile(fileUrl, fileName) {
|
||||
@@ -364,16 +416,21 @@ export function previewFile(fileUrl, fileName) {
|
||||
}
|
||||
} else {
|
||||
// Handle non-image file previews.
|
||||
if (extension === "pdf") {
|
||||
const embed = document.createElement("embed");
|
||||
const separator = fileUrl.indexOf('?') === -1 ? '?' : '&';
|
||||
embed.src = fileUrl + separator + 't=' + new Date().getTime();
|
||||
embed.type = "application/pdf";
|
||||
embed.style.width = "80vw";
|
||||
embed.style.height = "80vh";
|
||||
embed.style.border = "none";
|
||||
container.appendChild(embed);
|
||||
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(fileName)) {
|
||||
if (extension === "pdf") {
|
||||
// build a cache‐busted URL
|
||||
const separator = fileUrl.includes('?') ? '&' : '?';
|
||||
const urlWithTs = fileUrl + separator + 't=' + Date.now();
|
||||
|
||||
// open in a new tab (avoids CSP frame-ancestors)
|
||||
window.open(urlWithTs, "_blank");
|
||||
|
||||
// tear down the just-created modal
|
||||
const modal = document.getElementById("filePreviewModal");
|
||||
if (modal) modal.remove();
|
||||
|
||||
// stop further preview logic
|
||||
return;
|
||||
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(fileName)) {
|
||||
const video = document.createElement("video");
|
||||
video.src = fileUrl;
|
||||
video.controls = true;
|
||||
|
||||
@@ -1,44 +1,75 @@
|
||||
// folderShareModal.js
|
||||
// js/folderShareModal.js
|
||||
import { escapeHTML, showToast } from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
export function openFolderShareModal(folder) {
|
||||
// Remove any existing folder share modal
|
||||
// Remove any existing modal
|
||||
const existing = document.getElementById("folderShareModal");
|
||||
if (existing) existing.remove();
|
||||
|
||||
// Create the modal container
|
||||
// Build modal
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "folderShareModal";
|
||||
modal.classList.add("modal");
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content share-modal-content" style="width: 600px; max-width: 90vw;">
|
||||
<div class="modal-content share-modal-content" style="width:600px;max-width:90vw;">
|
||||
<div class="modal-header">
|
||||
<h3>${t("share_folder")}: ${escapeHTML(folder)}</h3>
|
||||
<span class="close-image-modal" id="closeFolderShareModal" title="Close">×</span>
|
||||
<span id="closeFolderShareModal" title="${t("close")}" class="close-image-modal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>${t("set_expiration")}</p>
|
||||
<select id="folderShareExpiration">
|
||||
<select id="folderShareExpiration" style="width:100%;padding:5px;">
|
||||
<option value="30">30 ${t("minutes")}</option>
|
||||
<option value="60" selected>60 ${t("minutes")}</option>
|
||||
<option value="120">120 ${t("minutes")}</option>
|
||||
<option value="180">180 ${t("minutes")}</option>
|
||||
<option value="240">240 ${t("minutes")}</option>
|
||||
<option value="1440">1 ${t("day")}</option>
|
||||
<option value="custom">${t("custom")}…</option>
|
||||
</select>
|
||||
<p>${t("password_optional")}</p>
|
||||
<input type="text" id="folderSharePassword" placeholder="${t("enter_password")}" style="width: 100%;"/>
|
||||
<br>
|
||||
<label>
|
||||
<input type="checkbox" id="folderShareAllowUpload"> ${t("allow_uploads")}
|
||||
|
||||
<div id="customFolderExpirationContainer" style="display:none;margin-top:10px;">
|
||||
<label for="customFolderExpirationValue">${t("duration")}:</label>
|
||||
<input type="number" id="customFolderExpirationValue" min="1" value="1" style="width:60px;margin:0 8px;"/>
|
||||
<select id="customFolderExpirationUnit">
|
||||
<option value="seconds">${t("seconds")}</option>
|
||||
<option value="minutes" selected>${t("minutes")}</option>
|
||||
<option value="hours">${t("hours")}</option>
|
||||
<option value="days">${t("days")}</option>
|
||||
</select>
|
||||
<p class="share-warning" style="color:#a33;font-size:0.9em;margin-top:5px;">
|
||||
${t("custom_duration_warning")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="margin-top:15px;">${t("password_optional")}</p>
|
||||
<input
|
||||
type="text"
|
||||
id="folderSharePassword"
|
||||
placeholder="${t("enter_password")}"
|
||||
style="width:100%;padding:5px;"
|
||||
/>
|
||||
|
||||
<label style="margin-top:10px;display:block;">
|
||||
<input type="checkbox" id="folderShareAllowUpload" />
|
||||
${t("allow_uploads")}
|
||||
</label>
|
||||
<br><br>
|
||||
<button id="generateFolderShareLinkBtn" class="btn btn-primary" style="margin-top: 10px;">${t("generate_share_link")}</button>
|
||||
<div id="folderShareLinkDisplay" style="margin-top: 10px; display: none;">
|
||||
|
||||
<button
|
||||
id="generateFolderShareLinkBtn"
|
||||
class="btn btn-primary"
|
||||
style="margin-top:15px;"
|
||||
>
|
||||
${t("generate_share_link")}
|
||||
</button>
|
||||
|
||||
<div id="folderShareLinkDisplay" style="margin-top:15px;display:none;">
|
||||
<p>${t("shareable_link")}</p>
|
||||
<input type="text" id="folderShareLinkInput" readonly style="width: 100%;"/>
|
||||
<button id="copyFolderShareLinkBtn" class="btn btn-primary" style="margin-top: 5px;">${t("copy_link")}</button>
|
||||
<input type="text" id="folderShareLinkInput" readonly style="width:100%;padding:5px;"/>
|
||||
<button id="copyFolderShareLinkBtn" class="btn btn-secondary" style="margin-top:5px;">
|
||||
${t("copy_link")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,62 +77,75 @@ export function openFolderShareModal(folder) {
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = "block";
|
||||
|
||||
// Close button handler
|
||||
document.getElementById("closeFolderShareModal").addEventListener("click", () => {
|
||||
modal.remove();
|
||||
});
|
||||
// Close
|
||||
document.getElementById("closeFolderShareModal")
|
||||
.addEventListener("click", () => modal.remove());
|
||||
|
||||
// Handler for generating the share link
|
||||
document.getElementById("generateFolderShareLinkBtn").addEventListener("click", () => {
|
||||
const expiration = document.getElementById("folderShareExpiration").value;
|
||||
const password = document.getElementById("folderSharePassword").value;
|
||||
const allowUpload = document.getElementById("folderShareAllowUpload").checked ? 1 : 0;
|
||||
|
||||
// Retrieve the CSRF token from the meta tag.
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
|
||||
if (!csrfToken) {
|
||||
showToast(t("csrf_error"));
|
||||
return;
|
||||
}
|
||||
// Post to the createFolderShareLink endpoint.
|
||||
fetch("/api/folder/createShareFolderLink.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder: folder,
|
||||
expirationMinutes: parseInt(expiration, 10),
|
||||
password: password,
|
||||
allowUpload: allowUpload
|
||||
// Toggle custom inputs
|
||||
document.getElementById("folderShareExpiration")
|
||||
.addEventListener("change", e => {
|
||||
document.getElementById("customFolderExpirationContainer")
|
||||
.style.display = e.target.value === "custom" ? "block" : "none";
|
||||
});
|
||||
|
||||
// Generate link
|
||||
document.getElementById("generateFolderShareLinkBtn")
|
||||
.addEventListener("click", () => {
|
||||
const sel = document.getElementById("folderShareExpiration");
|
||||
let value, unit;
|
||||
if (sel.value === "custom") {
|
||||
value = parseInt(document.getElementById("customFolderExpirationValue").value, 10);
|
||||
unit = document.getElementById("customFolderExpirationUnit").value;
|
||||
} else {
|
||||
value = parseInt(sel.value, 10);
|
||||
unit = "minutes";
|
||||
}
|
||||
|
||||
const password = document.getElementById("folderSharePassword").value;
|
||||
const allowUpload = document.getElementById("folderShareAllowUpload").checked ? 1 : 0;
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
|
||||
if (!csrfToken) {
|
||||
showToast(t("csrf_error"));
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/api/folder/createShareFolderLink.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder,
|
||||
expirationValue: value,
|
||||
expirationUnit: unit,
|
||||
password,
|
||||
allowUpload
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.token && data.link) {
|
||||
const shareUrl = data.link;
|
||||
const displayDiv = document.getElementById("folderShareLinkDisplay");
|
||||
const inputField = document.getElementById("folderShareLinkInput");
|
||||
inputField.value = shareUrl;
|
||||
displayDiv.style.display = "block";
|
||||
document.getElementById("folderShareLinkInput").value = data.link;
|
||||
document.getElementById("folderShareLinkDisplay").style.display = "block";
|
||||
showToast(t("share_link_generated"));
|
||||
} else {
|
||||
showToast(t("error_generating_share_link") + ": " + (data.error || t("unknown_error")));
|
||||
showToast(t("error_generating_share_link") + ": " + (data.error||t("unknown_error")));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error generating folder share link:", err);
|
||||
showToast(t("error_generating_share_link") + ": " + (err.error || t("unknown_error")));
|
||||
console.error(err);
|
||||
showToast(t("error_generating_share_link") + ": " + t("unknown_error"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Copy share link button handler
|
||||
document.getElementById("copyFolderShareLinkBtn").addEventListener("click", () => {
|
||||
const input = document.getElementById("folderShareLinkInput");
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
showToast(t("link_copied"));
|
||||
});
|
||||
// Copy
|
||||
document.getElementById("copyFolderShareLinkBtn")
|
||||
.addEventListener("click", () => {
|
||||
const inp = document.getElementById("folderShareLinkInput");
|
||||
inp.select();
|
||||
document.execCommand("copy");
|
||||
showToast(t("link_copied"));
|
||||
});
|
||||
}
|
||||
@@ -150,6 +150,13 @@ const translations = {
|
||||
"allow_uploads": "Allow Uploads",
|
||||
"share_link_generated": "Share Link Generated",
|
||||
"error_generating_share_link": "Error Generating Share Link",
|
||||
"custom": "Custom",
|
||||
"duration": "Duration",
|
||||
"seconds": "Seconds",
|
||||
"minutes": "Minutes",
|
||||
"hours": "Hours",
|
||||
"days": "Days",
|
||||
"custom_duration_warning": "⚠️ Using a long expiration may pose security risks. Use with caution.",
|
||||
|
||||
// Folder
|
||||
"folder_share": "Share Folder",
|
||||
@@ -166,12 +173,8 @@ const translations = {
|
||||
"user": "User:",
|
||||
"unknown_error": "Unknown Error",
|
||||
"link_copied": "Link Copied to Clipboard",
|
||||
"minutes": "minutes",
|
||||
"hours": "hours",
|
||||
"days": "days",
|
||||
"weeks": "weeks",
|
||||
"months": "months",
|
||||
"seconds": "seconds",
|
||||
|
||||
// Dark Mode Toggle
|
||||
"dark_mode_toggle": "Dark Mode",
|
||||
@@ -179,6 +182,22 @@ const translations = {
|
||||
"switch_to_light_mode": "Switch to light mode",
|
||||
"switch_to_dark_mode": "Switch to dark mode",
|
||||
|
||||
// Admin Panel
|
||||
"header_settings": "Header Settings",
|
||||
"shared_max_upload_size_bytes_title": "Shared Max Upload Size",
|
||||
"shared_max_upload_size_bytes": "Shared Max Upload Size (bytes)",
|
||||
"max_bytes_shared_uploads_note": "Enter maximum bytes allowed for shared-folder uploads",
|
||||
"manage_shared_links": "Manage Shared Links",
|
||||
"folder_shares": "Folder Shares",
|
||||
"file_shares": "File Shares",
|
||||
"loading": "Loading…",
|
||||
"error_loading_share_links": "Error loading share links",
|
||||
"share_deleted_successfully": "Share deleted successfully",
|
||||
"error_deleting_share": "Error deleting share",
|
||||
"password_protected": "Password protected",
|
||||
"no_shared_links_available": "No shared links available",
|
||||
|
||||
|
||||
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
|
||||
"admin_panel": "Admin Panel",
|
||||
"user_panel": "User Panel",
|
||||
@@ -239,7 +258,7 @@ const translations = {
|
||||
"ok": "OK",
|
||||
"show": "Show",
|
||||
"items_per_page": "items per page",
|
||||
"columns":"Columns",
|
||||
"columns": "Columns",
|
||||
"api_docs": "API Docs"
|
||||
},
|
||||
es: {
|
||||
@@ -806,7 +825,7 @@ const translations = {
|
||||
"prev": "Zurück",
|
||||
"next": "Weiter",
|
||||
"page": "Seite",
|
||||
"of": "von",
|
||||
"of": "von",
|
||||
|
||||
// Login Form keys:
|
||||
"login": "Anmelden",
|
||||
|
||||
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;
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
// src/controllers/adminController.php
|
||||
// src/controllers/AdminController.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
// src/controllers/authController.php
|
||||
// src/controllers/AuthController.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/AuthModel.php';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
// src/controllers/folderController.php
|
||||
// src/controllers/FolderController.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/FolderModel.php';
|
||||
@@ -76,7 +76,11 @@ class FolderController
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to create folders."]);
|
||||
http_response_code(403);
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"error" => "Read-only users are not allowed to create folders."
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -550,13 +554,18 @@ class FolderController
|
||||
body {
|
||||
background: #f2f2f2;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
padding: 0px 20px 20px 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -661,6 +670,28 @@ class FolderController
|
||||
font-size: 0.9rem;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background-color: #007BFF;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.pagination span {
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -670,7 +701,7 @@ class FolderController
|
||||
</div>
|
||||
<div class="container">
|
||||
<!-- Toggle Button -->
|
||||
<button id="toggleBtn" class="toggle-btn" onclick="toggleViewMode()">Switch to Gallery View</button>
|
||||
<button id="toggleBtn" class="toggle-btn">Switch to Gallery View</button>
|
||||
|
||||
<!-- List View Container -->
|
||||
<div id="listViewContainer">
|
||||
@@ -757,86 +788,14 @@ class FolderController
|
||||
<div class="footer">
|
||||
© <?php echo date("Y"); ?> FileRise. All rights reserved.
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// JavaScript for toggling view modes (list/gallery) and wiring up gallery clicks
|
||||
var viewMode = 'list';
|
||||
var token = '<?php echo addslashes($token); ?>';
|
||||
var filesData = <?php echo json_encode($files); ?>;
|
||||
|
||||
// Build the download URL base
|
||||
var downloadBase = window.location.origin +
|
||||
'/api/folder/downloadSharedFile.php?token=' +
|
||||
encodeURIComponent(token) +
|
||||
'&file=';
|
||||
|
||||
function toggleViewMode() {
|
||||
var listEl = document.getElementById('listViewContainer');
|
||||
var galleryEl = document.getElementById('galleryViewContainer');
|
||||
var btn = document.getElementById('toggleBtn');
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
// Wire up the toggle button
|
||||
document.getElementById('toggleBtn')
|
||||
.addEventListener('click', toggleViewMode);
|
||||
|
||||
function renderGalleryView() {
|
||||
var galleryContainer = document.getElementById('galleryViewContainer');
|
||||
var html = '<div class="shared-gallery-container">';
|
||||
|
||||
filesData.forEach(function(file) {
|
||||
var encodedName = encodeURIComponent(file);
|
||||
var fileUrl = downloadBase + encodedName;
|
||||
var ext = file.split('.').pop().toLowerCase();
|
||||
var thumb;
|
||||
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].indexOf(ext) >= 0) {
|
||||
thumb = '<img src="' + fileUrl + '" alt="' + file + '">';
|
||||
} else {
|
||||
thumb = '<span class="material-icons">insert_drive_file</span>';
|
||||
}
|
||||
|
||||
html +=
|
||||
'<div class="shared-gallery-card">' +
|
||||
'<div class="gallery-preview" data-url="' + fileUrl + '" style="cursor:pointer;">' +
|
||||
thumb +
|
||||
'</div>' +
|
||||
'<div class="gallery-info">' +
|
||||
'<span class="gallery-file-name">' + file + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
galleryContainer.innerHTML = html;
|
||||
|
||||
// Wire up each thumbnail click
|
||||
galleryContainer.querySelectorAll('.gallery-preview')
|
||||
.forEach(function(el) {
|
||||
el.addEventListener('click', function() {
|
||||
window.location.href = el.dataset.url;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Expose for manual invocation if needed
|
||||
window.renderGalleryView = renderGalleryView;
|
||||
});
|
||||
<!-- non-executing JSON payload, never blocked by CSP -->
|
||||
<script type="application/json" id="shared-data">
|
||||
{
|
||||
"token": <?php echo json_encode($token, JSON_HEX_TAG); ?>,
|
||||
"files": <?php echo json_encode($files, JSON_HEX_TAG); ?>
|
||||
}
|
||||
</script>
|
||||
<script src="/js/sharedFolderView.js" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -892,38 +851,63 @@ class FolderController
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
// Auth check
|
||||
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check that the user is not read-only.
|
||||
// Read-only check
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
$perms = loadUserPermissions($username);
|
||||
if ($username && !empty($perms['readOnly'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Read-only users are not allowed to create share folders."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Retrieve and decode POST input.
|
||||
$input = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$input || !isset($input['folder'])) {
|
||||
// Input
|
||||
$in = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$in || !isset($in['folder'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = trim($input['folder']);
|
||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
||||
$password = isset($input['password']) ? $input['password'] : "";
|
||||
$allowUpload = isset($input['allowUpload']) ? intval($input['allowUpload']) : 0;
|
||||
$folder = trim($in['folder']);
|
||||
$value = isset($in['expirationValue']) ? intval($in['expirationValue']) : 60;
|
||||
$unit = $in['expirationUnit'] ?? 'minutes';
|
||||
$password = $in['password'] ?? '';
|
||||
$allowUpload = intval($in['allowUpload'] ?? 0);
|
||||
|
||||
// Delegate to the model.
|
||||
$result = FolderModel::createShareFolderLink($folder, $expirationMinutes, $password, $allowUpload);
|
||||
echo json_encode($result);
|
||||
// Folder name validation
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
switch ($unit) {
|
||||
case 'seconds':
|
||||
$seconds = $value;
|
||||
break;
|
||||
case 'hours':
|
||||
$seconds = $value * 3600;
|
||||
break;
|
||||
case 'days':
|
||||
$seconds = $value * 86400;
|
||||
break;
|
||||
case 'minutes':
|
||||
default:
|
||||
$seconds = $value * 60;
|
||||
break;
|
||||
}
|
||||
|
||||
// Delegate
|
||||
$res = FolderModel::createShareFolderLink($folder, $seconds, $password, $allowUpload);
|
||||
echo json_encode($res);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -1094,4 +1078,34 @@ class FolderController
|
||||
header("Location: " . $redirectUrl);
|
||||
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
|
||||
// src/controllers/uploadController.php
|
||||
// src/controllers/UploadController.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/UploadModel.php';
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
// userController.php located in src/controllers/
|
||||
// UserController.php located in src/controllers/
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
||||
@@ -736,7 +736,7 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
||||
* @return array Returns an associative array with keys "token" and "expires" on success,
|
||||
* or "error" on failure.
|
||||
*/
|
||||
public static function createShareLink($folder, $file, $expirationMinutes = 60, $password = "") {
|
||||
public static function createShareLink($folder, $file, $expirationSeconds = 3600, $password = "") {
|
||||
// Validate folder if necessary (this can also be done in the controller).
|
||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
@@ -746,7 +746,7 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
||||
$token = bin2hex(random_bytes(16));
|
||||
|
||||
// Calculate expiration (Unix timestamp).
|
||||
$expires = time() + ($expirationMinutes * 60);
|
||||
$expires = time() + $expirationSeconds;
|
||||
|
||||
// Hash the password if provided.
|
||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
@@ -1253,4 +1253,29 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
||||
|
||||
return ["files" => $fileList, "globalTags" => $globalTags];
|
||||
}
|
||||
|
||||
public static function getAllShareLinks(): array
|
||||
{
|
||||
$shareFile = META_DIR . "share_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
return [];
|
||||
}
|
||||
$links = json_decode(file_get_contents($shareFile), true);
|
||||
return is_array($links) ? $links : [];
|
||||
}
|
||||
|
||||
public static function deleteShareLink(string $token): bool
|
||||
{
|
||||
$shareFile = META_DIR . "share_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
return false;
|
||||
}
|
||||
$links = json_decode(file_get_contents($shareFile), true);
|
||||
if (!is_array($links) || !isset($links[$token])) {
|
||||
return false;
|
||||
}
|
||||
unset($links[$token]);
|
||||
file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
require_once PROJECT_ROOT . '/config/config.php';
|
||||
|
||||
class FolderModel {
|
||||
class FolderModel
|
||||
{
|
||||
/**
|
||||
* Creates a folder under the specified parent (or in root) and creates an empty metadata file.
|
||||
*
|
||||
@@ -12,10 +13,11 @@ class FolderModel {
|
||||
* @return array Returns an array with a "success" key if the folder was created,
|
||||
* or an "error" key if an error occurred.
|
||||
*/
|
||||
public static function createFolder(string $folderName, string $parent = ""): array {
|
||||
public static function createFolder(string $folderName, string $parent = ""): array
|
||||
{
|
||||
$folderName = trim($folderName);
|
||||
$parent = trim($parent);
|
||||
|
||||
|
||||
// Validate folder name (only letters, numbers, underscores, dashes, and spaces allowed).
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
@@ -23,7 +25,7 @@ class FolderModel {
|
||||
if ($parent !== "" && !preg_match(REGEX_FOLDER_NAME, $parent)) {
|
||||
return ["error" => "Invalid parent folder name."];
|
||||
}
|
||||
|
||||
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
if ($parent !== "" && strtolower($parent) !== "root") {
|
||||
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $parent . DIRECTORY_SEPARATOR . $folderName;
|
||||
@@ -32,12 +34,12 @@ class FolderModel {
|
||||
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $folderName;
|
||||
$relativePath = $folderName;
|
||||
}
|
||||
|
||||
|
||||
// Check if the folder already exists.
|
||||
if (file_exists($fullPath)) {
|
||||
return ["error" => "Folder already exists."];
|
||||
}
|
||||
|
||||
|
||||
// Attempt to create the folder.
|
||||
if (mkdir($fullPath, 0755, true)) {
|
||||
// Create an empty metadata file for the new folder.
|
||||
@@ -50,52 +52,54 @@ class FolderModel {
|
||||
return ["error" => "Failed to create folder."];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates the metadata file path for a given folder.
|
||||
*
|
||||
* @param string $folder The relative folder path.
|
||||
* @return string The metadata file path.
|
||||
*/
|
||||
private static function getMetadataFilePath(string $folder): string {
|
||||
private static function getMetadataFilePath(string $folder): string
|
||||
{
|
||||
if (strtolower($folder) === 'root' || trim($folder) === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Deletes a folder if it is empty and removes its corresponding metadata.
|
||||
*
|
||||
* @param string $folder The folder name (relative to the upload directory).
|
||||
* @return array An associative array with "success" on success or "error" on failure.
|
||||
*/
|
||||
public static function deleteFolder(string $folder): array {
|
||||
public static function deleteFolder(string $folder): array
|
||||
{
|
||||
// Prevent deletion of "root".
|
||||
if (strtolower($folder) === 'root') {
|
||||
return ["error" => "Cannot delete root folder."];
|
||||
}
|
||||
|
||||
|
||||
// Validate folder name.
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
}
|
||||
|
||||
|
||||
// Build the full folder path.
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $folder;
|
||||
|
||||
|
||||
// Check if the folder exists and is a directory.
|
||||
if (!file_exists($folderPath) || !is_dir($folderPath)) {
|
||||
return ["error" => "Folder does not exist."];
|
||||
}
|
||||
|
||||
|
||||
// Prevent deletion if the folder is not empty.
|
||||
$items = array_diff(scandir($folderPath), array('.', '..'));
|
||||
if (count($items) > 0) {
|
||||
return ["error" => "Folder is not empty."];
|
||||
}
|
||||
|
||||
|
||||
// Attempt to delete the folder.
|
||||
if (rmdir($folderPath)) {
|
||||
// Remove corresponding metadata file.
|
||||
@@ -109,43 +113,45 @@ class FolderModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Renames a folder and updates related metadata files.
|
||||
*
|
||||
* @param string $oldFolder The current folder name (relative to UPLOAD_DIR).
|
||||
* @param string $newFolder The new folder name.
|
||||
* @return array Returns an associative array with "success" on success or "error" on failure.
|
||||
*/
|
||||
public static function renameFolder(string $oldFolder, string $newFolder): array {
|
||||
public static function renameFolder(string $oldFolder, string $newFolder): array
|
||||
{
|
||||
// Sanitize and trim folder names.
|
||||
$oldFolder = trim($oldFolder, "/\\ ");
|
||||
$newFolder = trim($newFolder, "/\\ ");
|
||||
|
||||
|
||||
// Validate folder names.
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) {
|
||||
return ["error" => "Invalid folder name(s)."];
|
||||
}
|
||||
|
||||
|
||||
// Build the full folder paths.
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$oldPath = $baseDir . DIRECTORY_SEPARATOR . $oldFolder;
|
||||
$newPath = $baseDir . DIRECTORY_SEPARATOR . $newFolder;
|
||||
|
||||
|
||||
// Validate that the old folder exists and new folder does not.
|
||||
if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) ||
|
||||
strpos(realpath($oldPath), realpath($baseDir)) !== 0 ||
|
||||
strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0) {
|
||||
strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0
|
||||
) {
|
||||
return ["error" => "Invalid folder path."];
|
||||
}
|
||||
|
||||
|
||||
if (!file_exists($oldPath) || !is_dir($oldPath)) {
|
||||
return ["error" => "Folder to rename does not exist."];
|
||||
}
|
||||
|
||||
|
||||
if (file_exists($newPath)) {
|
||||
return ["error" => "New folder name already exists."];
|
||||
}
|
||||
|
||||
|
||||
// Attempt to rename the folder.
|
||||
if (rename($oldPath, $newPath)) {
|
||||
// Update metadata: Rename all metadata files that have the old folder prefix.
|
||||
@@ -171,7 +177,8 @@ class FolderModel {
|
||||
* @param string $relative The relative path from the base directory.
|
||||
* @return array An array of folder paths (relative to the base).
|
||||
*/
|
||||
private static function getSubfolders(string $dir, string $relative = ''): array {
|
||||
private static function getSubfolders(string $dir, string $relative = ''): array
|
||||
{
|
||||
$folders = [];
|
||||
$items = scandir($dir);
|
||||
$safeFolderNamePattern = REGEX_FOLDER_NAME;
|
||||
@@ -198,7 +205,8 @@ class FolderModel {
|
||||
*
|
||||
* @return array An array of folder information arrays.
|
||||
*/
|
||||
public static function getFolderList(): array {
|
||||
public static function getFolderList(): array
|
||||
{
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$folderInfoList = [];
|
||||
|
||||
@@ -240,13 +248,14 @@ class FolderModel {
|
||||
return $folderInfoList;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Retrieves the share folder record for a given token.
|
||||
*
|
||||
* @param string $token The share folder token.
|
||||
* @return array|null The share folder record, or null if not found.
|
||||
*/
|
||||
public static function getShareFolderRecord(string $token): ?array {
|
||||
public static function getShareFolderRecord(string $token): ?array
|
||||
{
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
return null;
|
||||
@@ -257,8 +266,8 @@ class FolderModel {
|
||||
}
|
||||
return $shareLinks[$token];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Retrieves shared folder data based on a share token.
|
||||
*
|
||||
* @param string $token The share folder token.
|
||||
@@ -274,7 +283,8 @@ class FolderModel {
|
||||
* - 'totalPages': total pages,
|
||||
* or an 'error' key on failure.
|
||||
*/
|
||||
public static function getSharedFolderData(string $token, ?string $providedPass, int $page = 1, int $itemsPerPage = 10): array {
|
||||
public static function getSharedFolderData(string $token, ?string $providedPass, int $page = 1, int $itemsPerPage = 10): array
|
||||
{
|
||||
// Load the share folder record.
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
@@ -314,7 +324,7 @@ class FolderModel {
|
||||
return ["error" => "Shared folder not found."];
|
||||
}
|
||||
// Scan for files (only files).
|
||||
$allFiles = array_values(array_filter(scandir($realFolderPath), function($item) use ($realFolderPath) {
|
||||
$allFiles = array_values(array_filter(scandir($realFolderPath), function ($item) use ($realFolderPath) {
|
||||
return is_file($realFolderPath . DIRECTORY_SEPARATOR . $item);
|
||||
}));
|
||||
sort($allFiles);
|
||||
@@ -323,7 +333,7 @@ class FolderModel {
|
||||
$currentPage = min($page, $totalPages);
|
||||
$startIndex = ($currentPage - 1) * $itemsPerPage;
|
||||
$filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage);
|
||||
|
||||
|
||||
return [
|
||||
"record" => $record,
|
||||
"folder" => $folder,
|
||||
@@ -334,81 +344,72 @@ class FolderModel {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Creates a share link for a folder.
|
||||
*
|
||||
* @param string $folder The folder to share (relative to UPLOAD_DIR).
|
||||
* @param int $expirationMinutes The duration (in minutes) until the link expires.
|
||||
* @param string $password Optional password for the share.
|
||||
* @param int $allowUpload Optional flag (0 or 1) indicating whether uploads are allowed.
|
||||
* @return array An associative array with "token", "expires", and "link" on success, or "error" on failure.
|
||||
* @param string $folder The folder to share (relative to UPLOAD_DIR).
|
||||
* @param int $expirationSeconds How many seconds until expiry.
|
||||
* @param string $password Optional password.
|
||||
* @param int $allowUpload 0 or 1 whether uploads are allowed.
|
||||
* @return array ["token","expires","link"] on success, or ["error"].
|
||||
*/
|
||||
public static function createShareFolderLink(string $folder, int $expirationMinutes = 60, string $password = "", int $allowUpload = 0): array {
|
||||
// Validate folder name.
|
||||
public static function createShareFolderLink(string $folder, int $expirationSeconds = 3600, string $password = "", int $allowUpload = 0): array
|
||||
{
|
||||
// Validate folder
|
||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
}
|
||||
|
||||
// Generate secure token.
|
||||
// Token
|
||||
try {
|
||||
$token = bin2hex(random_bytes(16)); // 32 hex characters.
|
||||
$token = bin2hex(random_bytes(16));
|
||||
} catch (Exception $e) {
|
||||
return ["error" => "Could not generate token."];
|
||||
}
|
||||
|
||||
// Calculate expiration time.
|
||||
$expires = time() + ($expirationMinutes * 60);
|
||||
// Expiry
|
||||
$expires = time() + $expirationSeconds;
|
||||
|
||||
// Hash the password if provided.
|
||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
// Password hash
|
||||
$hashedPassword = $password !== "" ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
|
||||
// Define the share folder links file.
|
||||
// Load existing
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
$shareLinks = [];
|
||||
if (file_exists($shareFile)) {
|
||||
$data = file_get_contents($shareFile);
|
||||
$shareLinks = json_decode($data, true);
|
||||
if (!is_array($shareLinks)) {
|
||||
$shareLinks = [];
|
||||
$links = file_exists($shareFile)
|
||||
? json_decode(file_get_contents($shareFile), true) ?? []
|
||||
: [];
|
||||
|
||||
// Cleanup
|
||||
$now = time();
|
||||
foreach ($links as $k => $v) {
|
||||
if (!empty($v['expires']) && $v['expires'] < $now) {
|
||||
unset($links[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up expired share links.
|
||||
$currentTime = time();
|
||||
foreach ($shareLinks as $key => $link) {
|
||||
if (isset($link["expires"]) && $link["expires"] < $currentTime) {
|
||||
unset($shareLinks[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new share record.
|
||||
$shareLinks[$token] = [
|
||||
"folder" => $folder,
|
||||
"expires" => $expires,
|
||||
"password" => $hashedPassword,
|
||||
// Add new
|
||||
$links[$token] = [
|
||||
"folder" => $folder,
|
||||
"expires" => $expires,
|
||||
"password" => $hashedPassword,
|
||||
"allowUpload" => $allowUpload
|
||||
];
|
||||
|
||||
// Save the updated share links.
|
||||
if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT)) === false) {
|
||||
// Save
|
||||
if (file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT)) === false) {
|
||||
return ["error" => "Could not save share link."];
|
||||
}
|
||||
|
||||
// Determine the base URL.
|
||||
if (defined('BASE_URL') && !empty(BASE_URL) && strpos(BASE_URL, 'yourwebsite') === false) {
|
||||
$baseUrl = rtrim(BASE_URL, '/');
|
||||
} else {
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||
$host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : gethostbyname($_SERVER['SERVER_ADDR'] ?? 'localhost');
|
||||
$baseUrl = $protocol . "://" . $host;
|
||||
}
|
||||
// The share URL points to the shared folder page.
|
||||
$link = $baseUrl . "/api/folder/shareFolder.php?token=" . urlencode($token);
|
||||
|
||||
// Build URL
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||
$host = $_SERVER['HTTP_HOST'] ?? gethostbyname(gethostname());
|
||||
$baseUrl = $protocol . '://' . rtrim($host, '/');
|
||||
$link = $baseUrl . "/api/folder/shareFolder.php?token=" . urlencode($token);
|
||||
|
||||
return ["token" => $token, "expires" => $expires, "link" => $link];
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Retrieves information for a shared file from a shared folder link.
|
||||
*
|
||||
* @param string $token The share folder token.
|
||||
@@ -418,7 +419,8 @@ class FolderModel {
|
||||
* - "realFilePath": the absolute path to the file,
|
||||
* - "mimeType": the detected MIME type.
|
||||
*/
|
||||
public static function getSharedFileInfo(string $token, string $file): array {
|
||||
public static function getSharedFileInfo(string $token, string $file): array
|
||||
{
|
||||
// Load the share folder record.
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
@@ -457,14 +459,14 @@ class FolderModel {
|
||||
return ["error" => "Invalid file name."];
|
||||
}
|
||||
$file = basename($file);
|
||||
|
||||
|
||||
// Build the full file path.
|
||||
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
|
||||
$realFilePath = realpath($filePath);
|
||||
if ($realFilePath === false || strpos($realFilePath, $realFolderPath) !== 0 || !is_file($realFilePath)) {
|
||||
return ["error" => "File not found."];
|
||||
}
|
||||
|
||||
|
||||
$mimeType = mime_content_type($realFilePath);
|
||||
return [
|
||||
"realFilePath" => $realFilePath,
|
||||
@@ -479,11 +481,12 @@ class FolderModel {
|
||||
* @param array $fileUpload The $_FILES['fileToUpload'] array.
|
||||
* @return array An associative array with "success" on success or "error" on failure.
|
||||
*/
|
||||
public static function uploadToSharedFolder(string $token, array $fileUpload): array {
|
||||
public static function uploadToSharedFolder(string $token, array $fileUpload): array
|
||||
{
|
||||
// Define maximum file size and allowed extensions.
|
||||
$maxSize = 50 * 1024 * 1024; // 50 MB
|
||||
$allowedExtensions = ['jpg','jpeg','png','gif','pdf','doc','docx','txt','xls','xlsx','ppt','pptx','mp4','webm','mp3','mkv'];
|
||||
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'txt', 'xls', 'xlsx', 'ppt', 'pptx', 'mp4', 'webm', 'mp3', 'mkv'];
|
||||
|
||||
// Load the share folder record.
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
@@ -494,55 +497,55 @@ class FolderModel {
|
||||
return ["error" => "Invalid share token."];
|
||||
}
|
||||
$record = $shareLinks[$token];
|
||||
|
||||
|
||||
// Check expiration.
|
||||
if (time() > $record['expires']) {
|
||||
return ["error" => "This share link has expired."];
|
||||
}
|
||||
|
||||
|
||||
// Check whether uploads are allowed.
|
||||
if (empty($record['allowUpload']) || $record['allowUpload'] != 1) {
|
||||
return ["error" => "File uploads are not allowed for this share."];
|
||||
}
|
||||
|
||||
|
||||
// Validate file upload presence.
|
||||
if ($fileUpload['error'] !== UPLOAD_ERR_OK) {
|
||||
return ["error" => "File upload error. Code: " . $fileUpload['error']];
|
||||
}
|
||||
|
||||
|
||||
if ($fileUpload['size'] > $maxSize) {
|
||||
return ["error" => "File size exceeds allowed limit."];
|
||||
}
|
||||
|
||||
|
||||
$uploadedName = basename($fileUpload['name']);
|
||||
$ext = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $allowedExtensions)) {
|
||||
return ["error" => "File type not allowed."];
|
||||
}
|
||||
|
||||
|
||||
// Determine the target folder from the share record.
|
||||
$folderName = trim($record['folder'], "/\\");
|
||||
$targetFolder = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||
if (!empty($folderName) && strtolower($folderName) !== 'root') {
|
||||
$targetFolder .= $folderName;
|
||||
}
|
||||
|
||||
|
||||
// Verify target folder exists.
|
||||
$realTargetFolder = realpath($targetFolder);
|
||||
$uploadDirReal = realpath(UPLOAD_DIR);
|
||||
if ($realTargetFolder === false || strpos($realTargetFolder, $uploadDirReal) !== 0 || !is_dir($realTargetFolder)) {
|
||||
return ["error" => "Shared folder not found."];
|
||||
}
|
||||
|
||||
|
||||
// Generate a new filename (using uniqid and sanitizing the original name).
|
||||
$newFilename = uniqid() . "_" . preg_replace('/[^A-Za-z0-9_\-\.]/', '_', $uploadedName);
|
||||
$targetPath = $realTargetFolder . DIRECTORY_SEPARATOR . $newFilename;
|
||||
|
||||
|
||||
// Move the uploaded file.
|
||||
if (!move_uploaded_file($fileUpload['tmp_name'], $targetPath)) {
|
||||
return ["error" => "Failed to move the uploaded file."];
|
||||
}
|
||||
|
||||
|
||||
// --- Metadata Update ---
|
||||
// Determine metadata file.
|
||||
$metadataKey = (empty($folderName) || strtolower($folderName) === "root") ? "root" : $folderName;
|
||||
@@ -564,7 +567,32 @@ class FolderModel {
|
||||
"uploader" => $uploader
|
||||
];
|
||||
file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT));
|
||||
|
||||
|
||||
return ["success" => "File uploaded successfully.", "newFilename" => $newFilename];
|
||||
}
|
||||
}
|
||||
|
||||
public static function getAllShareFolderLinks(): array
|
||||
{
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
return [];
|
||||
}
|
||||
$links = json_decode(file_get_contents($shareFile), true);
|
||||
return is_array($links) ? $links : [];
|
||||
}
|
||||
|
||||
public static function deleteShareFolderLink(string $token): bool
|
||||
{
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
return false;
|
||||
}
|
||||
$links = json_decode(file_get_contents($shareFile), true);
|
||||
if (!is_array($links) || !isset($links[$token])) {
|
||||
return false;
|
||||
}
|
||||
unset($links[$token]);
|
||||
file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user