Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f162a7d0d7 | ||
|
|
3fc526df7f | ||
|
|
20422cf5a7 | ||
|
|
492bab36ca | ||
|
|
f2f7697994 | ||
|
|
13aa011632 | ||
|
|
1add160f5d | ||
|
|
87368143b5 | ||
|
|
939aa032f0 | ||
|
|
fbd21a035b | ||
|
|
2f391d11db | ||
|
|
8c70783d5a | ||
|
|
b4d6f01432 | ||
|
|
d48b15a5f4 | ||
|
|
d1726f0160 | ||
|
|
bd1841b788 | ||
|
|
bde35d1d31 | ||
|
|
8d6a1be777 | ||
|
|
56f34ba362 | ||
|
|
4d329e046f | ||
|
|
f3977153fb | ||
|
|
274bedd186 | ||
|
|
2e4dbe7f7f | ||
|
|
0334e443eb | ||
|
|
76f5ed5c96 | ||
|
|
18f588dc24 | ||
|
|
491c686762 | ||
|
|
25303df677 | ||
|
|
ae0d63b86f | ||
|
|
41ade2e205 | ||
|
|
0a9d332d60 | ||
|
|
1983f7705f | ||
|
|
6b2bf0ba70 | ||
|
|
6d9715169c | ||
|
|
0645a3712a | ||
|
|
ebc32ea965 | ||
|
|
078db33458 | ||
|
|
04f5cbe31f | ||
|
|
b5a7d8d559 | ||
|
|
58f8485b02 | ||
|
|
3e1da9c335 | ||
|
|
6bf6206e1c | ||
|
|
f9c60951c9 | ||
|
|
06b3f28df0 | ||
|
|
89f124250c | ||
|
|
66f13fd6a7 | ||
|
|
a81d9cb940 | ||
|
|
13b8871200 | ||
|
|
2792c05c1c | ||
|
|
6ccfc88acb | ||
|
|
7f1d59b33a | ||
|
|
e4e8b108d2 | ||
|
|
242661a9c9 | ||
|
|
ca3e2f316c | ||
|
|
6ff4aa5f34 | ||
|
|
1eb54b8e6e | ||
|
|
4a6c424540 | ||
|
|
d23d5b7f3f | ||
|
|
a48ba09f02 | ||
|
|
61357af203 | ||
|
|
e390a35e8a | ||
|
|
7e50ba1f70 | ||
|
|
cc41f8cc95 | ||
|
|
7c31b9689f | ||
|
|
461921b7bc | ||
|
|
3b58123584 | ||
|
|
cd9d7eb0ba | ||
|
|
c0c8d68dc4 | ||
|
|
2dfcb4062f | ||
|
|
d839b3ac1c | ||
|
|
766458f707 | ||
|
|
22cce5a898 | ||
|
|
75d3bf5a9b | ||
|
|
4ec4ba832f | ||
|
|
97b67593bc | ||
|
|
ec5c3fc452 | ||
|
|
853d8835d9 | ||
|
|
1d36d002c6 | ||
|
|
844976ef89 | ||
|
|
66e0d7ecbe | ||
|
|
a5fbcdef88 | ||
|
|
a897d1734f | ||
|
|
a9c4200827 | ||
|
|
97559873dc | ||
|
|
0683b27534 | ||
|
|
49c42e8096 | ||
|
|
ed39e112a9 | ||
|
|
25edab923a | ||
|
|
b8ae3c4402 | ||
|
|
fb537b1d61 | ||
|
|
90439022e3 | ||
|
|
b4c8738b8a | ||
|
|
e193bf9b13 | ||
|
|
a70d8fc2c7 | ||
|
|
d9f69d7917 | ||
|
|
28ac23c2f6 | ||
|
|
b06c49f213 | ||
|
|
8553efabc1 | ||
|
|
81a08ffd5b | ||
|
|
296dae96a5 | ||
|
|
337f529afd | ||
|
|
4360f2830a | ||
|
|
894cc938a5 | ||
|
|
01801ba950 | ||
|
|
5b592575a4 |
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
||||
# dockerignore
|
||||
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
.github/**
|
||||
Dockerfile*
|
||||
resources/
|
||||
node_modules/
|
||||
*.log
|
||||
tmp/
|
||||
.env
|
||||
.vscode/
|
||||
.DS_Store
|
||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
public/api.html linguist-documentation
|
||||
public/openapi.json linguist-documentation
|
||||
resources/ export-ignore
|
||||
.github/ export-ignore
|
||||
43
.github/workflows/sync-changelog.yml
vendored
Normal file
43
.github/workflows/sync-changelog.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Sync Changelog to Docker Repo
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'CHANGELOG.md'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout FileRise
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: file-rise
|
||||
|
||||
- name: Checkout filerise-docker
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: error311/filerise-docker
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
path: docker-repo
|
||||
|
||||
- name: Copy CHANGELOG.md
|
||||
run: |
|
||||
cp file-rise/CHANGELOG.md docker-repo/CHANGELOG.md
|
||||
|
||||
- name: Commit & push
|
||||
working-directory: docker-repo
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add CHANGELOG.md
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "chore: sync CHANGELOG.md from FileRise"
|
||||
git push origin main
|
||||
fi
|
||||
828
CHANGELOG.md
828
CHANGELOG.md
@@ -1,6 +1,828 @@
|
||||
# Changelog
|
||||
|
||||
## Shift Key Multi‑Selection Changes 4/10/2025
|
||||
## Changes 5/20/2025 1.3.6
|
||||
|
||||
- **domUtils.js**
|
||||
- `updateFileActionButtons`
|
||||
- Hide selection buttons (`Delete Files`, `Copy Files`, `Move Files` & `Download ZIP`) until file is selected.
|
||||
- Hides `Extract ZIP` until selecting zip files
|
||||
- Hide `Create File` button when file list items are selected.
|
||||
|
||||
---
|
||||
|
||||
## Changes 5/19/2025 1.3.5
|
||||
|
||||
### Added Folder strip & Create File
|
||||
|
||||
- **Folder strip in file list**
|
||||
- `loadFileList` now fetches sub-folders in parallel from `/api/folder/getFolderList.php`.
|
||||
- Filters to only *direct* children of the current folder, hiding `profile_pics` and `trash`.
|
||||
- Injects a new `.folder-strip-container` just below the Files In above (summary + slider).
|
||||
- Clicking a folder in the strip updates:
|
||||
- the breadcrumb (via `updateBreadcrumbTitle`)
|
||||
- the tree selection highlight
|
||||
- reloads `loadFileList` for the chosen folder.
|
||||
|
||||
- **Create File feature**
|
||||
- New “Create New File” button added to the file-actions toolbar and context menu.
|
||||
- New endpoint `public/api/file/createFile.php` (handled by `FileController`/`FileModel`):
|
||||
- Creates an empty file if it doesn’t already exist.
|
||||
- Appends an entry to `<folder>_metadata.json` with `uploaded` timestamp and `uploader`.
|
||||
- `fileActions.js`:
|
||||
- Implemented `handleCreateFile()` to show a modal, POST to the new endpoint, and refresh the list.
|
||||
- Added translations for `create_new_file` and `newfile_placeholder`.
|
||||
|
||||
---
|
||||
|
||||
## Changees 5/15/2025
|
||||
|
||||
### Drag‐and‐Drop Upload extended to File List
|
||||
|
||||
- **Forward file‐list drops**
|
||||
Dropping files onto the file‐list area (`#fileListContainer`) now re‐dispatches the same `drop` event to the upload card’s drop zone (`#uploadDropArea`)
|
||||
- **Visual feedback**
|
||||
Added a `.drop-hover` class on `#fileListContainer` during drag‐over for a dashed‐border + light‐background hover state to indicate it accepts file drops.
|
||||
|
||||
---
|
||||
|
||||
## Changes 5/14/2025 v1.3.4
|
||||
|
||||
### 1. Button Grouping (Bootstrap)
|
||||
|
||||
- Converted individual action buttons (`download`, `edit`, `rename`, `share`) in both **table view** and **gallery view** into a single Bootstrap button group for a cleaner, more compact UI.
|
||||
- Applied `btn-group` and `btn-sm` classes for consistent sizing and spacing.
|
||||
|
||||
### 2. Header Dropdown Replacement
|
||||
|
||||
- Replaced the standalone “User Panel” icon button with a **dropdown wrapper** (`.user-dropdown`) in the header.
|
||||
- Dropdown toggle now shows:
|
||||
- **Profile picture** (if set) or the Material “account_circle” icon
|
||||
- **Username** text (between avatar and caret)
|
||||
- Down-arrow caret span.
|
||||
|
||||
### 3. Menu Items Moved to Dropdown
|
||||
|
||||
- Moved previously standalone header buttons into the dropdown menu:
|
||||
- **User Panel** opens the modal
|
||||
- **Admin Panel** only shown when `data.isAdmin` *and* on `demo.filerise.net`
|
||||
- **API Docs** calls `openApiModal()`
|
||||
- **Logout** calls `triggerLogout()`
|
||||
- Each menu item now has a matching Material icon (e.g. `person`, `admin_panel_settings`, `description`, `logout`).
|
||||
|
||||
### 4. Profile Picture Support
|
||||
|
||||
- Added a new `/api/profile/uploadPicture.php` endpoint + `UserController::uploadPicture()` + corresponding `UserModel::setProfilePicture()`.
|
||||
- On **Open User Panel**, display:
|
||||
- Default avatar if none set
|
||||
- Current profile picture if available
|
||||
- In the **User Panel** modal:
|
||||
- Stylish “edit” overlay icon on the avatar to launch file picker
|
||||
- Auto-upload on file selection (no “Save” button click needed)
|
||||
- Preview updates immediately and header avatar refreshes live
|
||||
- Persisted in `users.txt` and re-fetched via `getCurrentUser.php`
|
||||
|
||||
### 5. API Docs & Logout Relocation
|
||||
|
||||
- Removed API Docs from User Panel
|
||||
- Removed “Logout” buttons from the header toolbar.
|
||||
- Both are now menu entries in the **User Dropdown**.
|
||||
|
||||
### 6. Admin Panel Conditional
|
||||
|
||||
- The **Admin Panel** button was:
|
||||
- Kept in the dropdown only when `data.isAdmin`
|
||||
- Removed entirely elsewhere.
|
||||
|
||||
### 7. Utility & Styling Tweaks
|
||||
|
||||
- Introduced a small `normalizePicUrl()` helper to strip stray colons and ensure a leading slash.
|
||||
- Hidden the scrollbar in the User Panel modal via:
|
||||
- Inline CSS (`scrollbar-width: none; -ms-overflow-style: none;`)
|
||||
- Global/WebKit rule for `::-webkit-scrollbar { display: none; }`
|
||||
- Made the User Panel modal fully responsive and vertically centered, with smooth dark-mode support.
|
||||
|
||||
### 8. File/List View & Gallery View Sliders
|
||||
|
||||
- **Unified “View‐Mode” Slider**
|
||||
Added a single slider panel (`#viewSliderContainer`) in the file‐list actions toolbar that switches behavior based on the current view mode:
|
||||
- **Table View**: shows a **Row Height** slider (min 31px, max 60px).
|
||||
- Adjusts the CSS variable `--file-row-height` to resize all `<tr>` heights.
|
||||
- Persists the chosen height in `localStorage`.
|
||||
- **Gallery View**: shows a **Columns** slider (min 1, max 6).
|
||||
- Updates the grid’s `grid-template-columns: repeat(N, 1fr)`.
|
||||
- Persists the chosen column count in `localStorage`.
|
||||
|
||||
- **Injection Point**
|
||||
The slider container is dynamically inserted (or updated) just before the folder summary (`#fileSummary`) in `loadFileList()`, ensuring a consistent position across both view modes.
|
||||
|
||||
- **Live Updates**
|
||||
Moving the slider thumb immediately updates the visible table row heights or gallery column layout without a full re‐render.
|
||||
|
||||
- **Styling & Alignment**
|
||||
- `#viewSliderContainer` uses `inline-flex` and `align-items: center` so that label, slider, and value text are vertically aligned with the other toolbar elements.
|
||||
- Reset margins/padding on the label and value span within `#viewSliderContainer` to eliminate any vertical misalignment.
|
||||
|
||||
### 9. Fixed new issues with Undefined username in header on profile pic change & TOTP Enabled not checked
|
||||
|
||||
**openUserPanel**
|
||||
|
||||
- **Rewritten entirely with DOM APIs** instead of `innerHTML` for any user-supplied text to eliminates “DOM text reinterpreted as HTML” warnings.
|
||||
- **Default avatar fallback**: now uses `'/assets/default-avatar.png'` whenever `profile_picture` is empty.
|
||||
- **TOTP checkbox initial state** is now set from the `totp_enabled` value returned by the server.
|
||||
- **Modal title sync** on reopen now updates the `(username)` correctly (no more “undefined” until refresh).
|
||||
- **Re-sync on reopen**: background color, avatar, TOTP checkbox and language selector all update when reopen the panel.
|
||||
|
||||
**updateAuthenticatedUI**
|
||||
|
||||
- **Username fix**: dropdown toggle now always uses `data.username` so the name never becomes `undefined` after uploading a picture.
|
||||
- **Profile URL update** via `fetchProfilePicture()` always writes into `localStorage` before rebuilding the header, ensuring avatar+name stay in sync instantly.
|
||||
- **Dropdown rebuild logic** tweaked to update the toggle’s innerHTML with both avatar and username on every call.
|
||||
|
||||
**UserModel::getUser**
|
||||
|
||||
- Switched to `explode(':', $line, 4)` to the fourth “profile_picture” field without clobbering the TOTP secret.
|
||||
- **Strip trailing colons** from the stored URL (`rtrim($parts[3], ':')`) so we never send `…png:` back to the client.
|
||||
- Returns an array with both `'username'` and `'profile_picture'`, matching what `getCurrentUser.php` needs.
|
||||
|
||||
### 10. setAttribute + encodeURI to avoid “DOM text reinterpreted as HTML” alerts
|
||||
|
||||
### 11. Fix duplicated Upload & Folder cards if they were added to header and page was refreshed
|
||||
|
||||
---
|
||||
|
||||
## Changes 5/8/2025
|
||||
|
||||
### Docker 🐳
|
||||
|
||||
- Ensure `/var/www/config` exists and is owned by `www-data` (chmod 750) so that `start.sh`’s `sed -i` updates to `config.php` work reliably
|
||||
|
||||
---
|
||||
|
||||
## Changes 5/8/2025 v1.3.3
|
||||
|
||||
### Enhancements
|
||||
|
||||
- **Admin API** (`updateConfig.php`):
|
||||
- Now merges incoming payload onto existing on-disk settings instead of overwriting blanks.
|
||||
- Preserves `clientId`, `clientSecret`, `providerUrl` and `redirectUri` when those fields are omitted or empty in the request.
|
||||
|
||||
- **Admin API** (`getConfig.php`):
|
||||
- Returns only a safe subset of admin settings (omits `clientSecret`) to prevent accidental exposure of sensitive data.
|
||||
|
||||
- **Frontend** (`auth.js`):
|
||||
- Update UI based on merged loginOptions from the server, ensuring blank or missing fields no longer revert your existing config.
|
||||
|
||||
- **Auth API** (`auth.php`):
|
||||
- Added `$oidc->addScope(['openid','profile','email']);` to OIDC flow. (This should resolve authentik issue)
|
||||
|
||||
---
|
||||
|
||||
## Changes 5/8/2025 v1.3.2
|
||||
|
||||
### config/config.php
|
||||
|
||||
- Added a default `define('AUTH_BYPASS', false)` at the top so the constant always exists.
|
||||
- Removed the static `AUTH_HEADER` fallback; instead read the adminConfig.json at the end of the file and:
|
||||
- Overwrote `AUTH_BYPASS` with the `loginOptions.authBypass` setting from disk.
|
||||
- Defined `AUTH_HEADER` (normalized, e.g. `"X_REMOTE_USER"`) based on `loginOptions.authHeaderName`.
|
||||
- Inserted a **proxy-only auto-login** block *before* the usual session/auth checks:
|
||||
If `AUTH_BYPASS` is true and the trusted header (`$_SERVER['HTTP_' . AUTH_HEADER]`) is present, bump the session, mark the user authenticated/admin, load their permissions, and skip straight to JSON output.
|
||||
- Relax filename validation regex to allow broader Unicode and special chars
|
||||
|
||||
### src/controllers/AdminController.php
|
||||
|
||||
- Ensured the returned `loginOptions` object always contains:
|
||||
- `authBypass` (boolean, default false)
|
||||
- `authHeaderName` (string, default `"X-Remote-User"`)
|
||||
- Read `authBypass` and `authHeaderName` from the nested `loginOptions` in the request payload.
|
||||
- Validated them (`authBypass` → bool; `authHeaderName` → non-empty string, fallback to `"X-Remote-User"`).
|
||||
- Included them when building the `$configUpdate` array to pass to the model.
|
||||
|
||||
### src/models/AdminModel.php
|
||||
|
||||
- Normalized `loginOptions.authBypass` to a boolean (default false).
|
||||
- Validated/truncated `loginOptions.authHeaderName` to a non-empty trimmed string (default `"X-Remote-User"`).
|
||||
- JSON-encoded and encrypted the full config, now including the two new fields.
|
||||
- After decrypting & decoding, normalized the loaded `loginOptions` to always include:
|
||||
- `authBypass` (bool)
|
||||
- `authHeaderName` (string, default `"X-Remote-User"`)
|
||||
- Left all existing defaults & validations for the original flags intact.
|
||||
|
||||
### public/js/adminPanel.js
|
||||
|
||||
- **Login Options** section:
|
||||
- Added a checkbox for **Disable All Built-in Logins (proxy only)** (`authBypass`).
|
||||
- Added a text input for **Auth Header Name** (`authHeaderName`).
|
||||
- In `handleSave()`:
|
||||
- Included the new `authBypass` and `authHeaderName` values in the payload sent to `updateConfig.php`.
|
||||
- In `openAdminPanel()`:
|
||||
- Initialized those inputs from `config.loginOptions.authBypass` and `config.loginOptions.authHeaderName`.
|
||||
|
||||
### public/js/auth.js
|
||||
|
||||
- In `loadAdminConfigFunc()`:
|
||||
- Stored `authBypass` and `authHeaderName` in `localStorage`.
|
||||
- In `checkAuthentication()`:
|
||||
- After a successful login check, called a new helper (`applyProxyBypassUI()`) which reads `localStorage.authBypass` and conditionally hides the entire login form/UI.
|
||||
- In the “not authenticated” branch, only shows the login form if `authBypass` is false.
|
||||
- No other core fetch/token logic changed; all existing flows remain intact.
|
||||
|
||||
### Security
|
||||
|
||||
- **Admin API**: `getConfig.php` now returns only a safe subset of admin settings (omits `clientSecret`) to prevent accidental exposure of sensitive data.
|
||||
|
||||
---
|
||||
|
||||
## Changes 5/4/2025 v1.3.1
|
||||
|
||||
### Modals
|
||||
|
||||
- **Added** a shared `.editor-close-btn` component for all modals:
|
||||
- File Tags
|
||||
- User Panel
|
||||
- TOTP Login & Setup
|
||||
- Change Password
|
||||
- **Truncated** long filenames in the File Tags modal header using CSS `text-overflow: ellipsis`.
|
||||
- **Resized** File Tags modal from 400px to 450px wide (with `max-width: 90vw` fallback).
|
||||
- **Capped** User Panel height at 381px and hidden scrollbars to eliminate layout jumps on hover.
|
||||
|
||||
### HTML
|
||||
|
||||
- **Moved** `<div id="loginForm">…</div>` out of `.main-wrapper` so the login form can show independently of the app shell.
|
||||
- **Added** `<div id="loadingOverlay"></div>` immediately inside `<body>` to cover the UI during auth checks.
|
||||
- **Inserted** inline `<style>` in `<head>` to:
|
||||
- Hide `.main-wrapper` by default.
|
||||
- Style `#loadingOverlay` as a full-viewport white overlay.
|
||||
|
||||
- **Added** `addUserModal`, `removeUserModal` & `renameFileModal` modals to `style="display:none;"`
|
||||
|
||||
### `main.js`
|
||||
|
||||
- **Extracted** `initializeApp()` helper to centralize post-auth startup (tag search, file list, drag-and-drop, folder tree, upload, trash/restore, admin config).
|
||||
- **Updated** DOMContentLoaded `checkAuthentication()` flow to call `initializeApp()` when already authenticated.
|
||||
- **Extended** `updateAuthenticatedUI()` to call `initializeApp()` after a fresh login so all UI modules re-hydrate.
|
||||
- **Enhanced** setup-mode in `checkAuthentication()`:
|
||||
- Show `#addUserModal` as a flex overlay (`style.display = 'flex'`).
|
||||
- Keep `.main-wrapper` hidden until setup completes.
|
||||
- **Added** post-setup handler in the Add-User modal’s save button:
|
||||
- Hide setup modal.
|
||||
- Show login form.
|
||||
- Keep app shell hidden.
|
||||
- Pre-fill and focus the new username in the login inputs.
|
||||
|
||||
### `auth.js` / Auth Logic
|
||||
|
||||
- **Refactored** `checkAuthentication()` to handle three states:
|
||||
1. **`data.setup`** remove overlay, hide main UI, show setup modal.
|
||||
2. **`data.authenticated`** remove overlay, call `updateAuthenticatedUI()`.
|
||||
3. **not authenticated** remove overlay, show login form, keep main UI hidden.
|
||||
- **Refined** `updateAuthenticatedUI()` to:
|
||||
- Remove loading overlay.
|
||||
- Show `.main-wrapper` and main operations.
|
||||
- Hide `#loginForm`.
|
||||
- Reveal header buttons.
|
||||
- Initialize dynamic header buttons (restore, admin, user-panel).
|
||||
- Call `initializeApp()` to load all modules after login.
|
||||
|
||||
---
|
||||
|
||||
## Changes 5/3/2025 v1.3.0
|
||||
|
||||
**Admin Panel Refactor & Enhancements**
|
||||
|
||||
### Moved from `authModals.js` to `adminPanel.js`
|
||||
|
||||
- Extracted all admin-related UI and logic out of `authModals.js`
|
||||
- Created a standalone `adminPanel.js` module
|
||||
- Initialized `openAdminPanel()` and `closeAdminPanel()` exports
|
||||
|
||||
### Responsive, Collapsible Sections
|
||||
|
||||
- Injected new CSS via JS (`adminPanelStyles`)
|
||||
- Default modal width: 50%
|
||||
- Small-screen override (`@media (max-width: 600px)`) to 90% width
|
||||
- Introduced `.section-header` / `.section-content` pattern
|
||||
- Click header to expand/collapse its content
|
||||
- Animated arrow via Material Icons
|
||||
- Indented and padded expanded content
|
||||
|
||||
### “Manage Shared Links” Feature
|
||||
|
||||
- Added new **Manage Shared Links** section to Admin Panel
|
||||
- Endpoint **GET** `/api/admin/readMetadata.php?file=…`
|
||||
- Reads `share_folder_links.json` & `share_links.json` under `META_DIR`
|
||||
- Endpoint **POST**
|
||||
- `/api/folder/deleteShareFolderLink.php`
|
||||
- `/api/file/deleteShareLink.php`
|
||||
- `loadShareLinksSection()` AJAX loader
|
||||
- Displays folder & file shares, expiry dates, upload-allowed, and 🔒 if password-protected
|
||||
- “🗑️” delete buttons refresh the list on success
|
||||
|
||||
### Dark-Mode & Theming Fixes
|
||||
|
||||
- Dark-mode CSS overrides for:
|
||||
- Modal border
|
||||
- `.btn-primary`, `.btn-secondary`
|
||||
- `.form-control` backgrounds & placeholders
|
||||
- Section headers & icons
|
||||
- Close button restyled to use shared **.editor-close-btn** look
|
||||
|
||||
### API and Controller changes
|
||||
|
||||
- Updated all endpoints to use correct controller casing
|
||||
- Renamed controller files to PascalCase (e.g. `adminController.php` to `AdminController.php`, `fileController.php` to `FileController.php`, `folderController.php` to `FolderController.php`)
|
||||
- Adjusted endpoint paths to match controller filenames
|
||||
- Fix FolderController readOnly create folder permission
|
||||
|
||||
### Additional changes
|
||||
|
||||
- Extend clean up expired shared entries
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/30/2025 v1.2.8
|
||||
|
||||
- **Added** PDF preview in `filePreview.js` (the `extension === "pdf"` block): replaced in-modal `<embed>` with `window.open(urlWithTs, "_blank")` and closed the modal to avoid CSP `frame-ancestors 'none'` restrictions.
|
||||
- **Added** `autofocus` attribute to the login form’s username input (`#loginUsername`) so the cursor is ready for typing on page load.
|
||||
- **Enhanced** login initialization with a `DOMContentLoaded` fallback that calls `loginUsername.focus()` (via `setTimeout`) if needed.
|
||||
- **Set** focus to the “New Username” field (`#newUsername`) when entering setup mode, hiding the login form and showing the Add-User modal.
|
||||
- **Implemented** Enter-key support in setup mode by attaching `attachEnterKeyListener("addUserModal", "saveUserBtn")`, allowing users to press Enter to submit the Add-User form.
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/28/2025
|
||||
|
||||
**Added**
|
||||
|
||||
- **Custom expiration** option to File Share modal
|
||||
- Users can specify a value + unit (seconds, minutes, hours, days)
|
||||
- Displays a warning when a custom duration is selected
|
||||
- **Custom expiration** option to Folder Share modal (same value+unit picker and warning)
|
||||
|
||||
**Changed**
|
||||
|
||||
- **API parameters** for both endpoints:
|
||||
- Replaced `expirationMinutes` with `expirationValue` + `expirationUnit`
|
||||
- Front-end now sends `{ expirationValue, expirationUnit }`
|
||||
- Back-end converts those into total seconds before saving
|
||||
- **UI**
|
||||
- FileShare and FolderShare modals updated to handle “Custom…” selection
|
||||
|
||||
**Updated Models & Controllers**
|
||||
|
||||
- **FileModel::createShareLink** now accepts expiration in seconds
|
||||
- **FolderModel::createShareFolderLink** now accepts expiration in seconds
|
||||
- **createShareLink.php** & **createShareFolderLink.php** updated to parse and convert new parameters
|
||||
|
||||
**Documentation**
|
||||
|
||||
- OpenAPI annotations for both endpoints updated to require `expirationValue` + `expirationUnit` (enum: seconds, minutes, hours, days)
|
||||
|
||||
## Changes 4/27/2025 v1.2.7
|
||||
|
||||
- **Select-All** checkbox now correctly toggles all `.file-checkbox` inputs
|
||||
- Updated `toggleAllCheckboxes(masterCheckbox)` to call `updateRowHighlight()` on each row so selections get the `.row-selected` highlight
|
||||
- **Master checkbox sync** in toolbar
|
||||
- Enhanced `updateFileActionButtons()` to set the header checkbox to checked, unchecked, or indeterminate based on how many files are selected
|
||||
- Fixed Pagination controls & Items-per-page dropdown
|
||||
- Fixed `#advancedSearchToggle` in both `renderFileTable()` and `renderGalleryView()`
|
||||
- **Shared folder gallery view logic**
|
||||
- Introduced new `public/js/sharedFolderView.js` containing all DOMContentLoaded wiring, `toggleViewMode()`, gallery rendering, and event listeners
|
||||
- Embedded a non-executing JSON payload in `shareFolder.php`
|
||||
- **`FolderController::shareFolder()` / `shareFolder.php`**
|
||||
- Removed all inline `onclick="…"` attributes and inline `<script>` blocks
|
||||
- Added `<script type="application/json" id="shared-data">…</script>` to export `$token` and `$files`
|
||||
- Added `<script src="/js/sharedFolderView.js" defer></script>` to load the external view logic
|
||||
- **Styling updates**
|
||||
- Added `.toggle-btn` CSS for blue header-style toggle button and applied it in JS
|
||||
- Added `.pagination a:hover { background-color: #0056b3; }` to match button hover
|
||||
- Tweaked `body` padding and `header h1` margins to reduce whitespace above header
|
||||
- Refactored `sharedFolderView.js:renderGalleryView()` to eliminate `innerHTML` usage; now uses `document.createElement` and `textContent` so filenames and URLs are fully escaped and CSP-safe
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/26/2025 1.2.6
|
||||
|
||||
**Apache / Dockerfile (CSP)**
|
||||
|
||||
- Enabled Apache’s `mod_headers` in the Dockerfile (`a2enmod headers ssl deflate expires proxy proxy_fcgi rewrite`)
|
||||
- Added a strong `Content-Security-Policy` header in the vhost configs to lock down allowed sources for scripts, styles, fonts, images, and connections
|
||||
|
||||
**index.html & CDN Includes**
|
||||
|
||||
- Applied Subresource Integrity (`integrity` + `crossorigin="anonymous"`) to all static CDN assets (Bootstrap CSS, CodeMirror CSS/JS, Resumable.js, DOMPurify, Fuse.js)
|
||||
- Omitted SRI on Google Fonts & Material Icons links (dynamic per-browser CSS)
|
||||
- Removed all inline `<script>` and `onclick` attributes; now all behaviors live in external JS modules
|
||||
|
||||
**auth.js (Logout Handling)**
|
||||
|
||||
- Moved the logout-on-`?logout=1` snippet from inline HTML into `auth.js`
|
||||
- In `DOMContentLoaded`, attached a `click` listener to `#logoutBtn` that POSTs to `/api/auth/logout.php` and reloads
|
||||
|
||||
**fileActions.js (Modal Button Handlers)**
|
||||
|
||||
- Externalized the cancel/download buttons for single-file and ZIP-download modals by adding `click` listeners in `fileActions.js`
|
||||
- Removed the inline `onclick` attributes from `#cancelDownloadFile` and `#confirmSingleDownloadButton` in the HTML
|
||||
- Ensured all file-action modals (delete, download, extract, copy, move, rename) now use JS event handlers instead of inline code
|
||||
|
||||
**domUtils.js**
|
||||
|
||||
- **Removed** all inline `onclick` and `onchange` attributes from:
|
||||
- `buildSearchAndPaginationControls` (advanced search toggle, prev/next buttons, items-per-page selector)
|
||||
- `buildFileTableHeader` (select-all checkbox)
|
||||
- `buildFileTableRow` (download, edit, preview, rename buttons)
|
||||
- **Retained** all original logic (file-type icon detection, shift-select, debounce, custom confirm modal, etc.)
|
||||
|
||||
**fileListView.js**
|
||||
|
||||
- **Stopped** generating inline `onclick` handlers in both table and gallery views.
|
||||
- **Added** `data-` attributes on actionable elements:
|
||||
- `data-download-name`, `data-download-folder`
|
||||
- `data-edit-name`, `data-edit-folder`
|
||||
- `data-rename-name`, `data-rename-folder`
|
||||
- `data-preview-url`, `data-preview-name`
|
||||
- IDs on controls: `#advancedSearchToggle`, `#searchInput`, `#prevPageBtn`, `#nextPageBtn`, `#selectAll`, `#itemsPerPageSelect`
|
||||
- **Introduced** `attachListControlListeners()` to bind all events via `addEventListener` immediately after rendering, preserving every interaction without inline code.
|
||||
|
||||
**Additional changes**
|
||||
|
||||
- **Security**: Added `frame-src 'self'` to the Content-Security-Policy header so that the embedded API docs iframe can load from our own origin without relaxing JS restrictions.
|
||||
- **Controller**: Updated `FolderController::shareFolder()` (folderController) to include the gallery-view toggle script block intact, ensuring the “Switch to Gallery View” button works when sharing folders.
|
||||
- **UI (fileListView.js)**: Refactored `renderGalleryView` to remove all inline `onclick=` handlers; switched to using data-attributes and `addEventListener()` for preview, download, edit and rename buttons, fully CSP-compliant.
|
||||
- Moved logout button handler out of inline `<script>` in `index.html` and into the `DOMContentLoaded` init in **main.js** (via `auth.js`), so it now attaches reliably after the CSRF token is loaded and DOM is ready.
|
||||
- Added Content-Security-Policy for `<Files "api.php">` block to allow embedding the ReDoc iframe.
|
||||
- Extracted inline ReDoc init into `public/js/redoc-init.js` and updated `public/api.php` to use deferred `<script>` tags.
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/25/2025
|
||||
|
||||
- Switch single‐file download to native `<a>` link (no JS buffering)
|
||||
- Keep spinner modal during ZIP creation and download blob on POST response
|
||||
- Replace text toggle with a single button showing sun/moon icons and hover tooltip
|
||||
|
||||
## Changes 4/24/2025 1.2.5
|
||||
|
||||
- Enhance README and wiki with expanded installation instructions
|
||||
- Adjusted Dockerfile’s Apache vhost to:
|
||||
- Alias `/uploads/` to `/var/www/uploads/` with PHP engine disabled and directory indexes off
|
||||
- Disable HTTP TRACE and tune keep-alive (On, max 100 requests, 5s timeout) and server Timeout (60s)
|
||||
- Add security headers (`X-Frame-Options`, `X-Content-Type-Options`, `X-XSS-Protection`, `Referrer-Policy`)
|
||||
- Enable `mod_deflate` compression for HTML, plain text, CSS, JS and JSON
|
||||
- Configure `mod_expires` caching for images (1 month), CSS (1 week) and JS (3 hour)
|
||||
- Deny access to hidden files (dot-files)
|
||||
~~- Add access control in public/.htaccess for api.html & openapi.json; update Nginx example in wiki~~
|
||||
- Remove obsolete folders from repo root
|
||||
- Embed API documentation (`api.php`) directly in the FileRise UI as a full-screen modal
|
||||
- Introduced `openApiModalBtn` in the user panel to launch the API modal
|
||||
- Added `#apiModal` container with a same-origin `<iframe src="api.php">` so session cookies authenticate automatically
|
||||
- Close control uses the existing `.editor-close-btn` for consistent styling and hover effects
|
||||
|
||||
- public/api.html has been replaced by the new api.php wrapper
|
||||
- **`public/api.php`**
|
||||
- Single PHP endpoint for both UI and spec
|
||||
- Enforces `$_SESSION['authenticated']`
|
||||
- Renders the Redoc API docs when accessed normally
|
||||
- Streams the JSON spec from `openapi.json.dist` when called as `api.php?spec=1`
|
||||
- Redirects unauthenticated users to `index.html?redirect=/api.php`
|
||||
- **Moved** `public/openapi.json` → `openapi.json.dist` (moved outside of `public/`) to prevent direct static access
|
||||
- **Dockerfile**: enabled required Apache modules for rewrite, security headers, proxying, caching and compression:
|
||||
|
||||
```dockerfile
|
||||
RUN a2enmod rewrite headers proxy proxy_fcgi expires deflate
|
||||
```
|
||||
|
||||
## Changes 4/23/2025 1.2.4
|
||||
|
||||
**AuthModel**
|
||||
|
||||
- **Added** `validateRememberToken(string $token): ?array`
|
||||
- Reads and decrypts `persistent_tokens.json`
|
||||
- Verifies token exists and hasn’t expired
|
||||
- Returns stored payload (`username`, `expiry`, `isAdmin`, etc.) or `null` if invalid
|
||||
|
||||
**authController (checkAuth)**
|
||||
|
||||
- **Enhanced** “remember-me” re-login path at top of `checkAuth()`
|
||||
- Calls `AuthModel::validateRememberToken()` when session is missing but `remember_me_token` cookie present
|
||||
- Repopulates `$_SESSION['authenticated']`, `username`, `isAdmin`, `folderOnly`, `readOnly`, `disableUpload` from payload
|
||||
- Regenerates session ID and CSRF token, then immediately returns JSON and exits
|
||||
|
||||
- **Updated** `userController.php`
|
||||
- Fixed totp isAdmin when session is missing but `remember_me_token` cookie present
|
||||
|
||||
- **loadCsrfToken()**
|
||||
- Now reads `X-CSRF-Token` response header first, falls back to JSON `csrf_token` if header absent
|
||||
- Updates `window.csrfToken`, `window.SHARE_URL`, and `<meta>` tags with the new values
|
||||
- **fetchWithCsrf(url, options)**
|
||||
- Sends `credentials: 'include'` and current `X-CSRF-Token` on every request
|
||||
- Handles “soft-failure” JSON (`{ csrf_expired: true, csrf_token }`): updates token and retries once without a 403 in DevTools
|
||||
- On HTTP 403 fallback: reads new token from header or `/api/auth/token.php`, updates token, and retries once
|
||||
|
||||
- **start.sh**
|
||||
- Session directory setup
|
||||
|
||||
- Always sends `credentials: 'include'` and `X-CSRF-Token: window.csrfToken` s
|
||||
- On HTTP 403, automatically fetches a fresh CSRF token (from the response header or `/api/auth/token.php`) and retries the request once
|
||||
- Always returns the real `Response` object (no more “clone.json” on every 200)
|
||||
- Now calls `fetchWithCsrf('/api/auth/token.php')` to guarantee a fresh token
|
||||
- Checks `res.ok`, then parses JSON to extract `csrf_token` and `share_url`
|
||||
- Updates both `window.csrfToken` and the `<meta name="csrf-token">` & `<meta name="share-url">` tags
|
||||
- Removed Old CSRF logic that cloned every successful response and parsed its JSON body
|
||||
- Removed Any “soft-failure” JSON peek on non-403 responses
|
||||
- Add missing permissions in `UserModel.php` for TOTP login.
|
||||
- **Prevent XSS in breadcrumbs**
|
||||
- Replaced `innerHTML` calls in `fileListTitle` with a new `updateBreadcrumbTitle()` helper that uses `textContent` + `DocumentFragment`.
|
||||
- Introduced `renderBreadcrumbFragment()` to build each breadcrumb segment as a `<span class="breadcrumb-link" data-folder="…">` node.
|
||||
- Added `setupBreadcrumbDelegation()` to handle clicks via event delegation on the container, eliminating per-element listeners.
|
||||
- Removed any raw HTML concatenation to satisfy CodeQL and ensure all breadcrumb text is safely escaped.
|
||||
|
||||
## Changes 4/22/2025 v1.2.3
|
||||
|
||||
- Support for custom PUID/PGID via `PUID`/`PGID` environment variables, replacing the need to run the container with `--user`
|
||||
- New `PUID` and `PGID` config options in the Unraid Community Apps template
|
||||
- Dockerfile:
|
||||
- startup (`start.sh`) now runs as root to write `/etc/php` & `/etc/apache2` configs
|
||||
- `www‑data` user is remapped at build‑time to the supplied `PUID:PGID`, then Apache drops privileges to that user
|
||||
- Unraid template: removed recommendation to use `--user`; replaced with `PUID`, `PGID`, and `Container Port` variables
|
||||
- “Permission denied” errors when forcing `--user 99:100` on Unraid by ensuring startup runs as root
|
||||
- Dockerfile silence group issue
|
||||
- `enableWebDAV` toggle in Admin Panel (default: disabled)
|
||||
- **Admin Panel enhancements**
|
||||
- New `enableWebDAV` boolean setting
|
||||
- New `sharedMaxUploadSize` numeric setting (bytes)
|
||||
- **Shared Folder upload size**
|
||||
- `sharedMaxUploadSize` is now enforced in `FolderModel::uploadToSharedFolder`
|
||||
- Upload form header on shared‑folder page dynamically shows “(X MB max size)”
|
||||
- **API updates**
|
||||
- `getConfig` and `updateConfig` endpoints now include `enableWebDAV` and `sharedMaxUploadSize`
|
||||
- Updated `AdminModel` & `AdminController` to persist and validate new settings
|
||||
- Enhanced `shareFolder()` view to pull from admin config and format the max‑upload‑size label
|
||||
- Restored the MIT license copyright line that was inadvertently removed.
|
||||
- Move .htaccess to public folder this was mistake since API refactor.
|
||||
- gitattributes to ignore resources/ & .github/ on export
|
||||
- Hardened `Dockerfile` permissions: all code files owned by `root:www-data` (dirs `755`, files `644`), only `uploads/`, `users/` and `metadata/` are writable by `www-data` (`775`)
|
||||
- `.dockerignore` entry to exclude the `.github` directory from build context
|
||||
- `start.sh`:
|
||||
- Creates and secures `metadata/log` for Apache logs
|
||||
- Dynamically creates and sets permissions on `uploads`, `users`, and `metadata` directories at startup
|
||||
- Apache VirtualHost updated to redirect `ErrorLog` and `CustomLog` into `/var/www/metadata/log`
|
||||
- docker: remove symlink add alias for uploads folder
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/21/2025 v1.2.2
|
||||
|
||||
### Added
|
||||
|
||||
- **`src/webdav/CurrentUser.php`**
|
||||
– Introduces a `CurrentUser` singleton to capture and expose the authenticated WebDAV username for use in other components.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`src/webdav/FileRiseDirectory.php`**
|
||||
– Constructor now takes three parameters (`$path`, `$user`, `$folderOnly`).
|
||||
– Implements “folder‑only” mode: non‑admin users only see their own subfolder under the uploads root.
|
||||
– Passes the current user through to `FileRiseFile` so that uploads/deletions are attributed correctly.
|
||||
|
||||
- **`src/webdav/FileRiseFile.php`**
|
||||
– Uses `CurrentUser::get()` when writing metadata to populate the `uploader` field.
|
||||
– Metadata helper (`updateMetadata`) now records both upload and modified timestamps along with the actual username.
|
||||
|
||||
- **`public/webdav.php`**
|
||||
– Adds a header‐shim at the top to pull Basic‑Auth credentials out of `Authorization` for all HTTP methods.
|
||||
– In the auth callback, sets the `CurrentUser` for the rest of the request.
|
||||
- Admins & unrestricted users see the full `/uploads` directory.
|
||||
- “Folder‑only” users are scoped to `/uploads/{username}`.
|
||||
– Configures SabreDAV with the new `FileRiseDirectory($rootPath, $user, $folderOnly)` signature and sets the base URI to `/webdav.php/`.
|
||||
|
||||
## Changes 4/19/2025 v1.2.1
|
||||
|
||||
- **Extended “Remember Me” cookie behavior**
|
||||
In `AuthController::finalizeLogin()`, after setting `remember_me_token` re‑issued the PHP session cookie with the same 30‑day expiry and called `session_regenerate_id(true)`.
|
||||
|
||||
- **Fetch URL fixes**
|
||||
Changed all front‑end `fetch("api/…")` calls to absolute paths `fetch("/api/…")` to avoid relative‑path 404/403 issues.
|
||||
|
||||
- **CSRF token refresh**
|
||||
Updated `submitLogin()` and both TOTP submission handlers to `async/await` a fresh CSRF token from `/api/auth/token.php` (with `credentials: "include"`) immediately before any POST.
|
||||
|
||||
- **submitLogin() overhaul**
|
||||
Refactored to:
|
||||
1. Fetch CSRF
|
||||
2. POST credentials to `/api/auth/auth.php`
|
||||
3. On `totp_required`, re‑fetch CSRF again before calling `openTOTPLoginModal()`
|
||||
4. Handle full logins vs. TOTP flows cleanly.
|
||||
|
||||
- **TOTP handlers update**
|
||||
In both the “Confirm TOTP” button flow and the auto‑submit on 6‑digit input:
|
||||
- Refreshed CSRF token before every `/api/totp_verify.php` call
|
||||
- Checked `response.ok` before parsing JSON
|
||||
- Improved `.catch` error handling
|
||||
|
||||
- **verifyTOTP() endpoint enhancement**
|
||||
Inside the **pending‑login** branch of `verifyTOTP()`:
|
||||
- Pulled `$_SESSION['pending_login_remember_me']`
|
||||
- If true, wrote the persistent token store, set `remember_me_token`, re‑issued the session cookie, and regenerated the session ID
|
||||
- Cleaned up pending session variables
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/18/2025
|
||||
|
||||
### fileListView.js
|
||||
|
||||
- Seed and persist `itemsPerPage` from `localStorage`
|
||||
- Use `window.itemsPerPage` for pagination in gallery
|
||||
- Enable search input filtering in gallery mode
|
||||
- Always re‑render the view‑toggle button on gallery load
|
||||
- Restore per‑card action buttons (download, edit, rename, share)
|
||||
- Assign real `value` to checkboxes and call `updateFileActionButtons()` on change
|
||||
- Update `changePage` and `changeItemsPerPage` to respect `viewMode`
|
||||
|
||||
### fileTags.js
|
||||
|
||||
- Import `renderFileTable` and `renderGalleryView`
|
||||
- Re‑render the list after saving a single‑file tag
|
||||
- Re‑render the list after saving multi‑file tags
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/17/2025
|
||||
|
||||
- Generate OpenAPI spec and API HTML docs
|
||||
- Fully auto‑generated OpenAPI spec (`openapi.json`) and interactive HTML docs (`api.html`) powered by Redoc.
|
||||
- .gitattributes added to mark (`openapi.json`) & (`api.html`) as documentation.
|
||||
- User Panel added API Docs link.
|
||||
- Adjusted remember_me_token.
|
||||
- Test pipeline
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/16 Refactor API endpoints and modularize controllers and models
|
||||
|
||||
- Reorganized project structure to separate API logic into dedicated controllers and models:
|
||||
- Created adminController, userController, fileController, folderController, uploadController, and authController.
|
||||
- Created corresponding models (AdminModel, UserModel, FileModel, FolderModel, UploadModel, AuthModel) for business logic.
|
||||
|
||||
- Consolidated API endpoints under the /public/api folder with subfolders for admin, auth, file, folder, and upload endpoints.
|
||||
|
||||
- Added inline OpenAPI annotations to document key endpoints (e.g., getConfig.php, updateConfig.php) for improved API documentation.
|
||||
|
||||
- Updated configuration retrieval and update logic in AdminModel and AdminController to handle OIDC and login option booleans consistently, fixing issues with basic auth settings not updating on the login page.
|
||||
|
||||
- Updated the client-side auth.js to correctly reference API endpoints (adjusted query selectors to reflect new document root) and load admin configuration from the updated API endpoints.
|
||||
|
||||
- Minor improvements to CSRF token handling, error logging, and overall code readability.
|
||||
|
||||
This refactor improves maintainability, testability, and documentation clarity across all API endpoints.
|
||||
|
||||
### Refactor fixes and adjustments
|
||||
|
||||
- Added fallback checks for disableFormLogin / disableBasicAuth / disableOIDCLogin when coming in either at the top level or under loginOptions.
|
||||
- Updated auth.js to read and store the nested loginOptions booleans correctly in localStorage, then show/hide the Basic‑Auth and OIDC buttons as configured.
|
||||
- Changed the logout controller to header("Location: /index.html?logout=1") so after /api/auth/logout.php it lands on the root index.html, not under /api/auth/.
|
||||
- Switched your share modal code to use a leading slash ("/api/file/share.php") so it generates absolute URLs instead of relative /share.php.
|
||||
- In the shared‑folder gallery, adjusted the client‑side image path to point at /uploads/... instead of /api/folder/uploads/...
|
||||
- Updated both AdminModel defaults and the AuthController to use the exact full path
|
||||
- Network Utilities Overhaul swapped out the old fetch wrapper for one that always reads the raw response, tries to JSON.parse it, and then either returns the parsed object on ok or throws it on error.
|
||||
- Adjusted your submitLogin .catch() to grab the thrown object (or string) and pass that through to showToast, so now “Invalid credentials” actually shows up.
|
||||
- Pulled the common session‑setup and “remember me” logic into two new helpers, finalizeLogin() (for AJAX/form/basic/TOTP) and finishBrowserLogin() (for OIDC redirects). That removed tons of duplication and ensures every path calls the same permission‑loading code.
|
||||
- Ensured that after you POST just a totp_code, we pick up pending_login_user/pending_login_secret, verify it, then immediately call finalizeLogin().
|
||||
- Expanded checkAuth.php Response now returns all three flags—folderOnly, readOnly, and disableUpload so client can handle every permission.
|
||||
- In auth.js’s updateAuthenticatedUI(), write all three flags into localStorage whenever you land on the app (OIDC, basic or form). That guarantees consistent behavior across page loads.
|
||||
- Made sure the OIDC handler reads the live config via AdminModel::getConfig() and pushes you through the TOTP flow if needed, then back to /index.html.
|
||||
- Dockerfile, custom-php.ini & start.sh moved into main repo for easier onboarding.
|
||||
- filerise-docker changed to dedicated CI/CD pipeline
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/15/2025
|
||||
|
||||
- Adjust Gallery View max columns based on screen size
|
||||
- Adjust headerTitle to update globally
|
||||
|
||||
## Changes 4/14/2025
|
||||
|
||||
- Fix Gallery View: medium screen devices get 3 max columns and small screen devices 2 max columns.
|
||||
- Ensure gallery view toggle button displays after refresh page.
|
||||
- Force resumable chunk size & fix chunk cleanup
|
||||
|
||||
### filePreview.js Enhancements
|
||||
|
||||
**Modal Layout Overhaul:**
|
||||
|
||||
- **Left Panel:** Holds zoom in/out controls at the top and the "prev" button at the bottom.
|
||||
- **Center Panel:** Always centers the preview image.
|
||||
- **Right Panel:** Contains rotate left/right controls at the top and the "next" button at the bottom.
|
||||
|
||||
**Consistent Control Presence:**
|
||||
|
||||
- Both left and right panels are always included. When there’s only one image, placeholders are inserted in place of missing navigation buttons to ensure the image remains centered and that rotate controls are always visible.
|
||||
|
||||
**Improved Transform Behavior:**
|
||||
|
||||
- Transformation values (scale and rotation) are reset on each navigation event, ensuring predictable behavior and consistent presentation.
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/13/2025 v1.1.3
|
||||
|
||||
- Decreased header height some more and clickable logo.
|
||||
- authModals.js fully updated with i18n.js keys.
|
||||
- main.js added Dark & Light mode i18n.js keys.
|
||||
- New Admin section Header Settings to change Header Title.
|
||||
- Admin Panel confirm unsaved changes.
|
||||
- Added translations and data attributes for almost all user-facing text
|
||||
- Extend i18n support: Add new translation keys for Download and Share modals
|
||||
|
||||
- **Slider Integration:**
|
||||
- Added a slider UI (range input, label, and value display) directly above the gallery grid.
|
||||
- The slider allows users to adjust the number of columns in the gallery from 1 to 6.
|
||||
- **Dynamic Grid Updates:**
|
||||
- The gallery grid’s CSS is updated in real time via the slider’s value by setting the grid-template-columns property.
|
||||
- As the slider value changes, the layout instantly reflects the new column count.
|
||||
- **Dynamic Image Resizing:**
|
||||
- Introduced a helper function (getMaxImageHeight) that calculates the maximum image height based on the current column count.
|
||||
- The max height of each image is updated immediately when the slider is adjusted to create a more dynamic display.
|
||||
- **Image Caching:**
|
||||
- Implemented an image caching mechanism using a global window.imageCache object.
|
||||
- Images are cached on load (via an onload event) to prevent unnecessary reloading, improving performance.
|
||||
- **Event Handling:**
|
||||
- The slider’s event listener is set up to update both the gallery grid layout and the dimensions of the thumbnails dynamically.
|
||||
- Share button event listeners remain attached for proper functionality across the updated gallery view.
|
||||
|
||||
- **Input Validation & Security:**
|
||||
- Used `filter_input()` to sanitize and validate incoming GET parameters (token, pass, page).
|
||||
- Validated file system paths using `realpath()` and ensured the shared folder lies within `UPLOAD_DIR`.
|
||||
- Escaped all dynamic outputs with `htmlspecialchars()` to prevent XSS.
|
||||
- **Share Link Verification:**
|
||||
- Loaded and validated share records from the JSON file.
|
||||
- Handled expiration and password protection (with proper HTTP status codes for errors).
|
||||
- **Pagination:**
|
||||
- Implemented pagination by slicing the full file list into a limited number of files per page (default of 10).
|
||||
- Calculated total pages and current page to create navigation links.
|
||||
- **View Toggle (List vs. Gallery):**
|
||||
- Added a toggle button that switches between a traditional list view and a gallery view.
|
||||
- Maintained two separate view containers (`#listViewContainer` and `#galleryViewContainer`) to support this switching.
|
||||
- **Gallery View with Image Caching:**
|
||||
- For the gallery view, implemented a JavaScript function that creates a grid of image thumbnails.
|
||||
- Each image uses a cache-busting query string on first load and caches its URL in a global `window.imageCache` for subsequent renders.
|
||||
- **Persistent Pagination Controls:**
|
||||
- Moved the pagination controls outside the individual view containers so that they remain visible regardless of the selected view.
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/12/2025
|
||||
|
||||
- Moved Gallery view toggle button into header.
|
||||
- Removed css entries that are not needed anymore for Gallery View Toggle.
|
||||
- Change search box text when enabling advanced search.
|
||||
- Advanced/Basic search button as material icon on same row as search bar.
|
||||
|
||||
### Advanced Search Implementation
|
||||
|
||||
- **Advanced Search Toggle:**
|
||||
- Added a global toggle (`window.advancedSearchEnabled`) and a UI button to switch between basic and advanced search modes.
|
||||
- The toggle button label changes between "Advanced Search" and "Basic Search" to reflect the active mode.
|
||||
|
||||
- **Fuse.js Integration Updates:**
|
||||
- Modified the `searchFiles()` function to conditionally include the `"content"` key in the Fuse.js keys only when advanced search mode is enabled.
|
||||
- Adjusted Fuse.js options by adding `ignoreLocation: true`, adjusting the `threshold`, and optionally assigning weights (e.g., a lower weight for `name` and a higher weight for `content`) to prioritize matches in file content.
|
||||
|
||||
- **Backend (PHP) Enhancements:**
|
||||
- Updated **getFileList.php** to read the content of text-based files (e.g., `.txt`, `.html`, `.md`, etc.) using `file_get_contents()`.
|
||||
- Added a `"content"` property to the JSON response for eligible files to allow for full-text search in advanced mode.
|
||||
|
||||
### Fuse.js Integration for Indexed Real-Time Searching**
|
||||
|
||||
- **Added Fuse.js Library:** Included Fuse.js via a CDN `<script>` tag to leverage its client‑side fuzzy search capabilities.
|
||||
- **Created searchFiles Helper Function:** Introduced a new function that uses Fuse.js to build an index and perform fuzzy searches over file properties (file name, uploader, and nested tag names).
|
||||
- **Transformed JSON Object to Array:** Updated the loadFileList() function to convert the returned file data into an array (if it isn’t already) and assign file names from JSON keys.
|
||||
- **Updated Rendering Functions:** Modified both renderFileTable() and renderGalleryView() to use the searchFiles() helper instead of a simple in‑array .filter(). This ensures that every search—real‑time by user input—is powered by Fuse.js’s indexed search.
|
||||
- **Enhanced Search Configuration:** Configured Fuse.js to search across multiple keys (file name, uploader, and tags) so that users can find files based on any of these properties.
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/11/2025
|
||||
|
||||
- Fixed fileDragDrop issue from previous update.
|
||||
- Fixed User Panel height changing unexpectedly on mouse over.
|
||||
- Improved JS file comments for better documentation.
|
||||
- Fixed userPermissions not updating after initial setting.
|
||||
- Disabled folder and file sharing for readOnly users.
|
||||
- Moved change password close button to the top right of the modal.
|
||||
- Updated upload regex pattern to be Unicode‑enabled and added additional security measures. [(#19)](https://github.com/error311/FileRise/issues/19)
|
||||
- Updated filename, folder, and username regex acceptance patterns.
|
||||
- Updated robthree/twofactorauth to v3 and endroid/qr-code to v5
|
||||
- Updated TOTP integration (namespace, enum, QR provider) accordingly
|
||||
- Updated docker image from 22.04 to 24.04 <https://github.com/error311/filerise-docker>
|
||||
- Ensure consistent session behavior
|
||||
- Fix totp_setup.php to use header-based CSRF token verification
|
||||
|
||||
---
|
||||
|
||||
## Shift Key Multi‑Selection Changes 4/10/2025 v1.1.1
|
||||
|
||||
- **Implemented Range Selection:**
|
||||
- Modified the `toggleRowSelection` function so that when the Shift key is held down, all rows between the last clicked (anchor) row (stored as `window.lastSelectedFileRow`) and the currently clicked row are selected.
|
||||
@@ -30,7 +852,7 @@
|
||||
- `shareFolder.php` updated to display format size.
|
||||
- Fix to prevent the filename text from overflowing its container in the gallery view.
|
||||
- Reduced header height.
|
||||
- Create Folder changed to Material Icon `edit`
|
||||
- Create Folder changed to Material Icon `create_new_folder`
|
||||
|
||||
---
|
||||
|
||||
@@ -735,7 +1557,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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
145
Dockerfile
Normal file
145
Dockerfile
Normal file
@@ -0,0 +1,145 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
#############################
|
||||
# Source Stage – copy your FileRise app
|
||||
#############################
|
||||
FROM ubuntu:24.04 AS appsource
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/* # clean up apt cache
|
||||
|
||||
RUN mkdir -p /var/www && rm -f /var/www/html/index.html
|
||||
COPY . /var/www
|
||||
|
||||
#############################
|
||||
# Composer Stage – install PHP dependencies
|
||||
#############################
|
||||
FROM composer:2 AS composer
|
||||
WORKDIR /app
|
||||
COPY --from=appsource /var/www/composer.json /var/www/composer.lock ./
|
||||
RUN composer install --no-dev --optimize-autoloader # production-ready autoloader
|
||||
|
||||
#############################
|
||||
# Final Stage – runtime image
|
||||
#############################
|
||||
FROM ubuntu:24.04
|
||||
LABEL by=error311
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
HOME=/root \
|
||||
LC_ALL=C.UTF-8 LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 TERM=xterm \
|
||||
UPLOAD_MAX_FILESIZE=5G POST_MAX_SIZE=5G TOTAL_UPLOAD_SIZE=5G \
|
||||
PERSISTENT_TOKENS_KEY=default_please_change_this_key \
|
||||
PUID=99 PGID=100
|
||||
|
||||
# Install Apache, PHP, and required extensions
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
apache2 php php-json php-curl php-zip php-mbstring php-gd php-xml \
|
||||
ca-certificates curl git openssl && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* # slim down image
|
||||
|
||||
# Remap www-data to the PUID/PGID provided for safe bind mounts
|
||||
RUN set -eux; \
|
||||
if [ "$(id -u www-data)" != "${PUID}" ]; then usermod -u "${PUID}" www-data; fi; \
|
||||
if [ "$(id -g www-data)" != "${PGID}" ]; then groupmod -g "${PGID}" www-data 2>/dev/null || true; fi; \
|
||||
usermod -g "${PGID}" www-data
|
||||
|
||||
# Copy config, code, and vendor
|
||||
COPY custom-php.ini /etc/php/8.3/apache2/conf.d/99-app-tuning.ini
|
||||
COPY --from=appsource /var/www /var/www
|
||||
COPY --from=composer /app/vendor /var/www/vendor
|
||||
|
||||
# ── ensure config/ is writable by www-data so sed -i can work ──
|
||||
RUN mkdir -p /var/www/config \
|
||||
&& chown -R www-data:www-data /var/www/config \
|
||||
&& chmod 750 /var/www/config
|
||||
|
||||
# Secure permissions: code read-only, only data dirs writable
|
||||
RUN chown -R root:www-data /var/www && \
|
||||
find /var/www -type d -exec chmod 755 {} \; && \
|
||||
find /var/www -type f -exec chmod 644 {} \; && \
|
||||
mkdir -p /var/www/public/uploads /var/www/users /var/www/metadata && \
|
||||
chown -R www-data:www-data /var/www/public/uploads /var/www/users /var/www/metadata && \
|
||||
chmod -R 775 /var/www/public/uploads /var/www/users /var/www/metadata # writable upload areas
|
||||
|
||||
# Apache site configuration
|
||||
RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
|
||||
<VirtualHost *:80>
|
||||
# Global settings
|
||||
TraceEnable off
|
||||
KeepAlive On
|
||||
MaxKeepAliveRequests 100
|
||||
KeepAliveTimeout 5
|
||||
Timeout 60
|
||||
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/public
|
||||
|
||||
# Security headers for all responses
|
||||
<IfModule mod_headers.c>
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self'; frame-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
|
||||
</IfModule>
|
||||
|
||||
# Compression
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json
|
||||
</IfModule>
|
||||
|
||||
# Cache static assets
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive on
|
||||
ExpiresByType image/jpeg "access plus 1 month"
|
||||
ExpiresByType image/png "access plus 1 month"
|
||||
ExpiresByType text/css "access plus 1 week"
|
||||
ExpiresByType application/javascript "access plus 3 hour"
|
||||
</IfModule>
|
||||
|
||||
# Protect uploads directory
|
||||
Alias /uploads/ /var/www/uploads/
|
||||
<Directory "/var/www/uploads/">
|
||||
Options -Indexes
|
||||
AllowOverride None
|
||||
<IfModule mod_php7.c>
|
||||
php_flag engine off
|
||||
</IfModule>
|
||||
<IfModule mod_php.c>
|
||||
php_flag engine off
|
||||
</IfModule>
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# Public directory
|
||||
<Directory "/var/www/public">
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
DirectoryIndex index.html index.php
|
||||
</Directory>
|
||||
|
||||
# Deny access to hidden files
|
||||
<FilesMatch "^\.">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
<Files "api.php">
|
||||
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.redoc.ly; style-src 'self' 'unsafe-inline'; worker-src 'self' https://cdn.redoc.ly blob:; connect-src 'self'; img-src 'self' data: blob:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';"
|
||||
</Files>
|
||||
|
||||
ErrorLog /var/www/metadata/log/error.log
|
||||
CustomLog /var/www/metadata/log/access.log combined
|
||||
</VirtualHost>
|
||||
EOF
|
||||
|
||||
# Enable required modules
|
||||
RUN a2enmod rewrite headers proxy proxy_fcgi expires deflate ssl
|
||||
|
||||
EXPOSE 80 443
|
||||
COPY start.sh /usr/local/bin/start.sh
|
||||
RUN chmod +x /usr/local/bin/start.sh
|
||||
|
||||
CMD ["/usr/local/bin/start.sh"]
|
||||
1
LICENSE
1
LICENSE
@@ -1,5 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 SeNS
|
||||
Copyright (c) 2025 FileRise
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
||||
94
README.md
94
README.md
@@ -1,7 +1,7 @@
|
||||
# FileRise
|
||||
|
||||
**Elevate your File Management** – A modern, self-hosted web file manager.
|
||||
Upload, organize, and share files through a sleek web interface. **FileRise** is lightweight yet powerful: think of it as your personal cloud drive that you control. With drag-and-drop uploads, in-browser editing, secure user logins (with SSO and 2FA support), and one-click sharing, **FileRise** makes file management on your server a breeze.
|
||||
Upload, organize, and share files or folders through a sleek web interface. **FileRise** is lightweight yet powerful: think of it as your personal cloud drive that you control. With drag-and-drop uploads, in-browser editing, secure user logins (with SSO and 2FA support), and one-click sharing, **FileRise** makes file management on your server a breeze.
|
||||
|
||||
**4/3/2025 Video demo:**
|
||||
|
||||
@@ -20,19 +20,23 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
|
||||
|
||||
- 🗃️ **Folder Sharing & File Sharing:** Easily share entire folders via secure, expiring public links. Folder shares can be password-protected, and shared folders support file uploads from outside users with a separate, secure upload mechanism. Folder listings are paginated (10 items per page) with navigation controls, and file sizes are displayed in MB for clarity. Share files with others using one-time or expiring public links (with password protection if desired) – convenient for sending individual files without exposing the whole app.
|
||||
|
||||
- 🔌 **WebDAV Support:** Mount FileRise as a network drive **or use it head‑less from the CLI**. Standard WebDAV operations (upload / download / rename / delete) work in Cyberduck, WinSCP, GNOME Files, Finder, etc., and you can also script against it with `curl` – see the [WebDAV](https://github.com/error311/FileRise/wiki/WebDAV) + [curl](https://github.com/error311/FileRise/wiki/Accessing-FileRise-via-curl%C2%A0(WebDAV)) quick‑start for examples. Folder‑Only users are restricted to their personal directory, while admins and unrestricted users have full access.
|
||||
|
||||
- 📚 **API Documentation:** Fully auto‑generated OpenAPI spec (`openapi.json`) and interactive HTML docs (`api.html`) powered by Redoc.
|
||||
|
||||
- 📝 **Built-in Editor & Preview:** View images, videos, audio, and PDFs inline with a preview modal – no need to download just to see them. Edit text/code files right in your browser with a CodeMirror-based editor featuring syntax highlighting and line numbers. Great for config files or notes – tweak and save changes without leaving FileRise.
|
||||
|
||||
- 🏷️ **Tags & Search:** Categorize your files with color-coded tags (labels) and later find them easily. The global search bar filters by filename or tag, making it simple to locate that “important” document in seconds. Tag management is built-in – create, reuse, or remove tags as needed.
|
||||
- 🏷️ **Tags & Search:** Categorize your files with color-coded tags and locate them instantly using our indexed real-time search. Easily switch to Advanced Search mode to enable fuzzy matching not only across file names, tags, and uploader fields but also within the content of text files—helping you find that “important” document even if you make a typo or need to search deep within the file.
|
||||
|
||||
- 🔒 **User Authentication & User Permissions:** Secure your portal with username/password login. Supports multiple users – create user accounts (admin UI provided) for family or team members. User permissions such as User “Folder Only” feature assigns each user a dedicated folder within the root directory, named after their username, restricting them from viewing or modifying other directories. User Read Only and Disable Upload are additional permissions. FileRise also integrates with Single Sign-On (OIDC) providers (e.g., OAuth2/OIDC for Google/Authentik/Keycloak) and offers optional TOTP two-factor auth for extra security.
|
||||
|
||||
- 🎨 **Responsive UI (Dark/Light Mode):** FileRise is mobile-friendly out of the box – manage files from your phone or tablet with a responsive layout. Choose between Dark mode or Light theme, or let it follow your system preference. The interface remembers your preferences (layout, items per page, last visited folder, etc.) for a personalized experience each time.
|
||||
|
||||
- 🌐 **Internationalization & Localization:** FileRise supports multiple languages via an integrated i18n system. Users can switch languages through a user panel dropdown, and their choice is saved in local storage for a consistent experience across sessions. Currently available in English, Spanish, and French—please report any translation issues you encounter.
|
||||
- 🌐 **Internationalization & Localization:** FileRise supports multiple languages via an integrated i18n system. Users can switch languages through a user panel dropdown, and their choice is saved in local storage for a consistent experience across sessions. Currently available in English, Spanish, French & German—please report any translation issues you encounter.
|
||||
|
||||
- 🗑️ **Trash & File Recovery:** Mistakenly deleted files? No worries – deleted items go to the Trash instead of immediate removal. Admins can restore files from Trash or empty it to free space. FileRise auto-purges old trash entries (default 3 days) to keep your storage tidy.
|
||||
|
||||
- ⚙️ **Lightweight & Self-Contained:** FileRise runs on PHP 8.1+ with no external database required – data is stored in files (users, metadata) for simplicity. It’s a single-folder web app you can drop into any Apache/PHP server or run as a container. Docker & Unraid ready: use our pre-built image for a hassle-free setup. Memory and CPU footprint is minimal, yet the app scales to thousands of files with pagination and sorting features.
|
||||
- ⚙️ **Lightweight & Self‑Contained:** FileRise runs on PHP 8.1+ with no external database required – data is stored in files (users, metadata) for simplicity. It’s a single‑folder web app you can drop into any Apache/PHP server or run as a container. Docker & Unraid ready: use our pre‑built image for a hassle‑free setup. Memory and CPU footprint is minimal, yet the app scales to thousands of files with pagination and sorting features.
|
||||
|
||||
(For a full list of features and detailed changelogs, see the [Wiki](https://github.com/error311/FileRise/wiki), [changelog](https://github.com/error311/FileRise/blob/master/CHANGELOG.md) or the [releases](https://github.com/error311/FileRise/releases) pages.)
|
||||
|
||||
@@ -58,8 +62,6 @@ If you have Docker installed, you can get FileRise up and running in minutes:
|
||||
docker pull error311/filerise-docker:latest
|
||||
```
|
||||
|
||||
*(For Apple Silicon (M1/M2) users, use --platform linux/amd64 tag until multi-arch support is added.)*
|
||||
|
||||
- **Run a container:**
|
||||
|
||||
``` bash
|
||||
@@ -106,16 +108,16 @@ FileRise will be accessible at `http://localhost:8080` (or your server’s IP).
|
||||
|
||||
If you prefer to run FileRise on a traditional web server (LAMP stack or similar):
|
||||
|
||||
- **Requirements:** PHP 8.1 or higher, Apache (with mod_php) or another web server configured for PHP. Ensure PHP extensions json, curl, and zip are enabled. No database needed.
|
||||
- **Requirements:** PHP 8.3 or higher, Apache (with mod_php) or another web server configured for PHP. Ensure PHP extensions json, curl, and zip are enabled. No database needed.
|
||||
- **Download Files:** Clone this repo or download the [latest release archive](https://github.com/error311/FileRise/releases).
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/error311/FileRise.git
|
||||
```
|
||||
|
||||
Place the files into your web server’s directory (e.g., `/var/www/html/filerise`). It can be in a subfolder (just adjust the `BASE_URL` in config as below).
|
||||
Place the files into your web server’s directory (e.g., `/var/www/`). It can be in a subfolder (just adjust the `BASE_URL` in config as below).
|
||||
|
||||
- **Composer Dependencies:** If you plan to use OIDC (SSO login), install Composer and run `composer install` in the FileRise directory. (This pulls in a couple of PHP libraries like jumbojett/openid-connect for OAuth support.) If you skip this, FileRise will still work, but OIDC login won’t be available.
|
||||
- **Composer Dependencies:** Install Composer and run `composer install` in the FileRise directory. (This pulls in a couple of PHP libraries like jumbojett/openid-connect for OAuth support.)
|
||||
|
||||
- **Folder Permissions:** Ensure the server can write to the following directories (create them if they don’t exist):
|
||||
|
||||
@@ -145,6 +147,51 @@ Now navigate to the FileRise URL in your browser. On first load, you’ll be pro
|
||||
|
||||
---
|
||||
|
||||
## Quick‑start: Mount via WebDAV
|
||||
|
||||
Once FileRise is running, you must enable WebDAV in admin panel to access it.
|
||||
|
||||
```bash
|
||||
# Linux (GVFS/GIO)
|
||||
gio mount dav://demo@your-host/webdav.php/
|
||||
|
||||
# macOS (Finder → Go → Connect to Server…)
|
||||
dav://demo@your-host/webdav.php/
|
||||
|
||||
```
|
||||
|
||||
### Windows (File Explorer)
|
||||
|
||||
- Open **File Explorer** → Right-click **This PC** → **Map network drive…**
|
||||
- Choose a drive letter (e.g., `Z:`).
|
||||
- In **Folder**, enter:
|
||||
|
||||
```text
|
||||
https://your-host/webdav.php/
|
||||
```
|
||||
|
||||
- Check **Connect using different credentials**, and enter your FileRise username and password.
|
||||
- Click **Finish**. The drive will now appear under **This PC**.
|
||||
|
||||
> **Important:**
|
||||
> Windows requires HTTPS (SSL) for WebDAV connections by default.
|
||||
> If your server uses plain HTTP, you must adjust a registry setting:
|
||||
>
|
||||
> 1. Open **Registry Editor** (`regedit.exe`).
|
||||
> 2. Navigate to:
|
||||
>
|
||||
> ```text
|
||||
> HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters
|
||||
> ```
|
||||
>
|
||||
> 3. Find or create a `DWORD` value named **BasicAuthLevel**.
|
||||
> 4. Set its value to `2`.
|
||||
> 5. Restart the **WebClient** service or reboot your computer.
|
||||
|
||||
📖 For a full guide (including SSL setup, HTTP workaround, and troubleshooting), see the [WebDAV Usage Wiki](https://github.com/error311/FileRise/wiki/WebDAV).
|
||||
|
||||
---
|
||||
|
||||
## FAQ / Troubleshooting
|
||||
|
||||
- **“Upload failed” or large files not uploading:** Make sure `TOTAL_UPLOAD_SIZE` in config and PHP’s `post_max_size` / `upload_max_filesize` are all set high enough. For extremely large files, you might also need to increase max_execution_time in PHP or rely on the resumable upload feature in smaller chunks.
|
||||
@@ -171,12 +218,39 @@ Areas where you can help: translations, bug fixes, UI improvements, or building
|
||||
|
||||
## Community and Support
|
||||
|
||||
- **Reddit:** [r/selfhosted: FileRise Discussion](https://www.reddit.com/r/selfhosted/comments/1jl01pi/introducing_filerise_a_modern_selfhosted_file/) – (Announcement and user feedback thread).
|
||||
- **Reddit:** [r/selfhosted: FileRise Discussion](https://www.reddit.com/r/selfhosted/comments/1kfxo9y/filerise_v131_major_updates_sneak_peek_at_whats/) – (Announcement and user feedback thread).
|
||||
- **Unraid Forums:** [FileRise Support Thread](https://forums.unraid.net/topic/187337-support-filerise/) – for Unraid-specific support or issues.
|
||||
- **GitHub Discussions:** Use the Q&A category for any setup questions, and the Ideas category to suggest enhancements.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### PHP Libraries
|
||||
|
||||
- **[jumbojett/openid-connect-php](https://github.com/jumbojett/OpenID-Connect-PHP)** (v^1.0.0)
|
||||
- **[phpseclib/phpseclib](https://github.com/phpseclib/phpseclib)** (v~3.0.7)
|
||||
- **[robthree/twofactorauth](https://github.com/RobThree/TwoFactorAuth)** (v^3.0)
|
||||
- **[endroid/qr-code](https://github.com/endroid/qr-code)** (v^5.0)
|
||||
- **[sabre/dav](https://github.com/sabre-io/dav)** (^4.4)
|
||||
|
||||
### Client-Side Libraries
|
||||
|
||||
- **Google Fonts** – [Roboto](https://fonts.google.com/specimen/Roboto) and **Material Icons** ([Google Material Icons](https://fonts.google.com/icons))
|
||||
- **[Bootstrap](https://getbootstrap.com/)** (v4.5.2)
|
||||
- **[CodeMirror](https://codemirror.net/)** (v5.65.5) – For code editing functionality.
|
||||
- **[Resumable.js](https://github.com/23/resumable.js/)** (v1.1.0) – For file uploads.
|
||||
- **[DOMPurify](https://github.com/cure53/DOMPurify)** (v2.4.0) – For sanitizing HTML.
|
||||
- **[Fuse.js](https://fusejs.io/)** (v6.6.2) – For indexed, fuzzy searching.
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Based on [uploader](https://github.com/sensboston/uploader) by @sensboston.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is open-source under the MIT License. That means you’re free to use, modify, and distribute **FileRise**, with attribution. We hope you find it useful and contribute back!
|
||||
|
||||
86
addUser.php
86
addUser.php
@@ -1,86 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
|
||||
// Determine if we are in setup mode:
|
||||
// - Query parameter setup=1 is passed
|
||||
// - And users.txt is either missing or empty (zero bytes or trimmed content is empty)
|
||||
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
|
||||
if ($isSetup && (!file_exists($usersFile) || filesize($usersFile) == 0 || trim(file_get_contents($usersFile)) === '')) {
|
||||
// Allow initial admin creation without session checks.
|
||||
$setupMode = true;
|
||||
} else {
|
||||
$setupMode = false;
|
||||
// In non-setup mode, check CSRF token and require admin privileges.
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
if (
|
||||
!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
||||
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true
|
||||
) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Get input data from JSON.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
$newUsername = trim($data["username"] ?? "");
|
||||
$newPassword = trim($data["password"] ?? "");
|
||||
|
||||
// In setup mode, force the new user to be admin.
|
||||
if ($setupMode) {
|
||||
$isAdmin = "1";
|
||||
} else {
|
||||
$isAdmin = !empty($data["isAdmin"]) ? "1" : "0"; // "1" for admin, "0" for regular user.
|
||||
}
|
||||
|
||||
// Validate input.
|
||||
if (!$newUsername || !$newPassword) {
|
||||
echo json_encode(["error" => "Username and password required"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate username using preg_match (allow letters, numbers, underscores, dashes, and spaces).
|
||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $newUsername)) {
|
||||
echo json_encode(["error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure users.txt exists.
|
||||
if (!file_exists($usersFile)) {
|
||||
file_put_contents($usersFile, '');
|
||||
}
|
||||
|
||||
// Check if username already exists.
|
||||
$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($existingUsers as $line) {
|
||||
list($storedUser, $storedHash, $storedRole) = explode(':', trim($line));
|
||||
if ($newUsername === $storedUser) {
|
||||
echo json_encode(["error" => "User already exists"]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the password.
|
||||
$hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
|
||||
|
||||
// Prepare new user line.
|
||||
$newUserLine = $newUsername . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL;
|
||||
|
||||
// In setup mode, overwrite users.txt; otherwise, append to it.
|
||||
if ($setupMode) {
|
||||
file_put_contents($usersFile, $newUserLine);
|
||||
} else {
|
||||
file_put_contents($usersFile, $newUserLine, FILE_APPEND);
|
||||
}
|
||||
|
||||
echo json_encode(["success" => "User added successfully"]);
|
||||
?>
|
||||
262
auth.php
262
auth.php
@@ -1,262 +0,0 @@
|
||||
<?php
|
||||
require_once 'vendor/autoload.php';
|
||||
require_once 'config.php';
|
||||
|
||||
// Only send the Content-Type header; CORS and related headers are handled via .htaccess.
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Global exception handler: logs errors and returns a generic error message.
|
||||
set_exception_handler(function ($e) {
|
||||
error_log("Unhandled exception: " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(["error" => "Internal Server Error"]);
|
||||
exit();
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper: Get the user's role from users.txt.
|
||||
*/
|
||||
function getUserRole($username) {
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
if (file_exists($usersFile)) {
|
||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
$parts = explode(":", trim($line));
|
||||
if (count($parts) >= 3 && $parts[0] === $username) {
|
||||
return trim($parts[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* --- OIDC Authentication Flow --- */
|
||||
// Detect either ?oidc=… or a callback that only has ?code=
|
||||
$oidcAction = $_GET['oidc'] ?? null;
|
||||
if (!$oidcAction && isset($_GET['code'])) {
|
||||
$oidcAction = 'callback';
|
||||
}
|
||||
if ($oidcAction) {
|
||||
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
||||
if (file_exists($adminConfigFile)) {
|
||||
$enc = file_get_contents($adminConfigFile);
|
||||
$dec = decryptData($enc, $encryptionKey);
|
||||
$cfg = $dec !== false ? json_decode($dec, true) : [];
|
||||
} else {
|
||||
$cfg = [];
|
||||
}
|
||||
$oidc_provider_url = $cfg['oidc']['providerUrl'] ?? 'https://your-oidc-provider.com';
|
||||
$oidc_client_id = $cfg['oidc']['clientId'] ?? 'YOUR_CLIENT_ID';
|
||||
$oidc_client_secret = $cfg['oidc']['clientSecret'] ?? 'YOUR_CLIENT_SECRET';
|
||||
// Use your production domain for redirect URI.
|
||||
$oidc_redirect_uri = $cfg['oidc']['redirectUri'] ?? 'https://yourdomain.com/auth.php?oidc=callback';
|
||||
|
||||
$oidc = new Jumbojett\OpenIDConnectClient(
|
||||
$oidc_provider_url,
|
||||
$oidc_client_id,
|
||||
$oidc_client_secret
|
||||
);
|
||||
$oidc->setRedirectURL($oidc_redirect_uri);
|
||||
|
||||
if ($oidcAction === 'callback') {
|
||||
try {
|
||||
$oidc->authenticate();
|
||||
$username = $oidc->requestUserInfo('preferred_username');
|
||||
|
||||
// Check if this user has a TOTP secret.
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
$totp_secret = null;
|
||||
if (file_exists($usersFile)) {
|
||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
$parts = explode(":", trim($line));
|
||||
if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
|
||||
$totp_secret = decryptData($parts[3], $encryptionKey);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($totp_secret) {
|
||||
// Hold pending login & prompt for TOTP.
|
||||
$_SESSION['pending_login_user'] = $username;
|
||||
$_SESSION['pending_login_secret'] = $totp_secret;
|
||||
header("Location: index.html?totp_required=1");
|
||||
exit();
|
||||
}
|
||||
|
||||
// No TOTP → finalize login.
|
||||
session_regenerate_id(true);
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $username;
|
||||
$_SESSION["isAdmin"] = (getUserRole($username) === "1");
|
||||
$_SESSION["folderOnly"] = loadUserPermissions($username);
|
||||
|
||||
header("Location: index.html");
|
||||
exit();
|
||||
} catch (Exception $e) {
|
||||
error_log("OIDC authentication error: " . $e->getMessage());
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Authentication failed."]);
|
||||
exit();
|
||||
}
|
||||
} else {
|
||||
// Initiate OIDC authentication.
|
||||
try {
|
||||
$oidc->authenticate();
|
||||
exit();
|
||||
} catch (Exception $e) {
|
||||
error_log("OIDC initiation error: " . $e->getMessage());
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Authentication initiation failed."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Fallback: Form-based Authentication --- */
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
$maxAttempts = 5;
|
||||
$lockoutTime = 30 * 60; // 30 minutes
|
||||
$attemptsFile = USERS_DIR . 'failed_logins.json';
|
||||
$failedLogFile = USERS_DIR . 'failed_login.log';
|
||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
||||
|
||||
function loadFailedAttempts($file) {
|
||||
if (file_exists($file)) {
|
||||
$data = json_decode(file_get_contents($file), true);
|
||||
if (is_array($data)) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function saveFailedAttempts($file, $data) {
|
||||
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX);
|
||||
}
|
||||
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
$currentTime = time();
|
||||
$failedAttempts = loadFailedAttempts($attemptsFile);
|
||||
|
||||
if (isset($failedAttempts[$ip])) {
|
||||
$attemptData = $failedAttempts[$ip];
|
||||
if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) {
|
||||
http_response_code(429);
|
||||
echo json_encode(["error" => "Too many failed login attempts. Please try again later."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
function authenticate($username, $password) {
|
||||
global $usersFile, $encryptionKey;
|
||||
if (!file_exists($usersFile)) {
|
||||
return false;
|
||||
}
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(':', trim($line));
|
||||
if (count($parts) < 3) continue;
|
||||
if ($username === $parts[0] && password_verify($password, $parts[1])) {
|
||||
$result = ['role' => $parts[2]];
|
||||
$result['totp_secret'] = (isset($parts[3]) && !empty($parts[3]))
|
||||
? decryptData($parts[3], $encryptionKey)
|
||||
: null;
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
$username = trim($data["username"] ?? "");
|
||||
$password = trim($data["password"] ?? "");
|
||||
$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true;
|
||||
|
||||
if (!$username || !$password) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Username and password are required"]);
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
||||
exit();
|
||||
}
|
||||
|
||||
$user = authenticate($username, $password);
|
||||
if ($user !== false) {
|
||||
if (!empty($user['totp_secret'])) {
|
||||
// If TOTP code is missing or malformed, indicate that TOTP is required.
|
||||
if (empty($data['totp_code']) || !preg_match('/^\d{6}$/', $data['totp_code'])) {
|
||||
// ← STORE pending user & secret so recovery can see it
|
||||
$_SESSION['pending_login_user'] = $username;
|
||||
$_SESSION['pending_login_secret'] = $user['totp_secret'];
|
||||
echo json_encode([
|
||||
"totp_required" => true,
|
||||
"message" => "TOTP code required"
|
||||
]);
|
||||
exit();
|
||||
} else {
|
||||
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
|
||||
$providedCode = trim($data['totp_code']);
|
||||
if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) {
|
||||
echo json_encode(["error" => "Invalid TOTP code"]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($failedAttempts[$ip])) {
|
||||
unset($failedAttempts[$ip]);
|
||||
saveFailedAttempts($attemptsFile, $failedAttempts);
|
||||
}
|
||||
session_regenerate_id(true);
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $username;
|
||||
$_SESSION["isAdmin"] = ($user['role'] === "1");
|
||||
$_SESSION["folderOnly"] = loadUserPermissions($username);
|
||||
|
||||
if ($rememberMe) {
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiry = time() + (30 * 24 * 60 * 60);
|
||||
$persistentTokens = [];
|
||||
if (file_exists($persistentTokensFile)) {
|
||||
$encryptedContent = file_get_contents($persistentTokensFile);
|
||||
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
||||
$persistentTokens = json_decode($decryptedContent, true);
|
||||
if (!is_array($persistentTokens)) {
|
||||
$persistentTokens = [];
|
||||
}
|
||||
}
|
||||
$persistentTokens[$token] = [
|
||||
"username" => $username,
|
||||
"expiry" => $expiry,
|
||||
"isAdmin" => ($_SESSION["isAdmin"] === true)
|
||||
];
|
||||
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
|
||||
// Define $secure based on whether HTTPS is enabled
|
||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
"status" => "ok",
|
||||
"success" => "Login successful",
|
||||
"isAdmin" => $_SESSION["isAdmin"],
|
||||
"folderOnly"=> $_SESSION["folderOnly"],
|
||||
"username" => $_SESSION["username"]
|
||||
]);
|
||||
} else {
|
||||
if (isset($failedAttempts[$ip])) {
|
||||
$failedAttempts[$ip]['count']++;
|
||||
$failedAttempts[$ip]['last_attempt'] = $currentTime;
|
||||
} else {
|
||||
$failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime];
|
||||
}
|
||||
saveFailedAttempts($attemptsFile, $failedAttempts);
|
||||
$logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL;
|
||||
file_put_contents($failedLogFile, $logLine, FILE_APPEND);
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Invalid credentials"]);
|
||||
}
|
||||
?>
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
// changePassword.php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if (!$username) {
|
||||
echo json_encode(["error" => "No username in session"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// CSRF token check.
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get POST data.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
$oldPassword = trim($data["oldPassword"] ?? "");
|
||||
$newPassword = trim($data["newPassword"] ?? "");
|
||||
$confirmPassword = trim($data["confirmPassword"] ?? "");
|
||||
|
||||
// Validate input.
|
||||
if (!$oldPassword || !$newPassword || !$confirmPassword) {
|
||||
echo json_encode(["error" => "All fields are required."]);
|
||||
exit;
|
||||
}
|
||||
if ($newPassword !== $confirmPassword) {
|
||||
echo json_encode(["error" => "New passwords do not match."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Path to users file.
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
if (!file_exists($usersFile)) {
|
||||
echo json_encode(["error" => "Users file not found"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Read current users.
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$userFound = false;
|
||||
$newLines = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(':', trim($line));
|
||||
// Expect at least 3 parts: username, hashed password, and role.
|
||||
if (count($parts) < 3) {
|
||||
// Skip invalid lines.
|
||||
$newLines[] = $line;
|
||||
continue;
|
||||
}
|
||||
$storedUser = $parts[0];
|
||||
$storedHash = $parts[1];
|
||||
$storedRole = $parts[2];
|
||||
// Preserve TOTP secret if it exists.
|
||||
$totpSecret = (count($parts) >= 4) ? $parts[3] : "";
|
||||
|
||||
if ($storedUser === $username) {
|
||||
$userFound = true;
|
||||
// Verify the old password.
|
||||
if (!password_verify($oldPassword, $storedHash)) {
|
||||
echo json_encode(["error" => "Old password is incorrect."]);
|
||||
exit;
|
||||
}
|
||||
// Hash the new password.
|
||||
$newHashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
|
||||
// Rebuild the line with the new hash and preserve TOTP secret if present.
|
||||
if ($totpSecret !== "") {
|
||||
$newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole . ":" . $totpSecret;
|
||||
} else {
|
||||
$newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole;
|
||||
}
|
||||
} else {
|
||||
$newLines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$userFound) {
|
||||
echo json_encode(["error" => "User not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Save updated users file.
|
||||
if (file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL)) {
|
||||
echo json_encode(["success" => "Password updated successfully."]);
|
||||
} else {
|
||||
echo json_encode(["error" => "Could not update password."]);
|
||||
}
|
||||
?>
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Check if users.txt is empty or doesn't exist.
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
|
||||
// In production, you might log that the system is in setup mode.
|
||||
error_log("checkAuth: users file not found or empty; entering setup mode.");
|
||||
echo json_encode(["setup" => true]);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Check session authentication.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["authenticated" => false]);
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get a user's role from users.txt.
|
||||
* Returns the role as a string (e.g. "1") or null if not found.
|
||||
*/
|
||||
function getUserRole($username) {
|
||||
global $usersFile;
|
||||
if (file_exists($usersFile)) {
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(":", trim($line));
|
||||
if (count($parts) >= 3 && $parts[0] === $username) {
|
||||
return trim($parts[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine if TOTP is enabled by checking users.txt.
|
||||
$totp_enabled = false;
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if ($username) {
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(":", trim($line));
|
||||
// Assuming first field is username and fourth (if exists) is the TOTP secret.
|
||||
if ($parts[0] === $username) {
|
||||
if (isset($parts[3]) && trim($parts[3]) !== "") {
|
||||
$totp_enabled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use getUserRole() to determine admin status.
|
||||
// We cast the role to an integer so that "1" (string) is treated as true.
|
||||
$userRole = getUserRole($username);
|
||||
$isAdmin = ((int)$userRole === 1);
|
||||
|
||||
// Build and return the JSON response.
|
||||
$response = [
|
||||
"authenticated" => true,
|
||||
"isAdmin" => $isAdmin,
|
||||
"totp_enabled" => $totp_enabled,
|
||||
"username" => $username,
|
||||
"folderOnly" => isset($_SESSION["folderOnly"]) ? $_SESSION["folderOnly"] : false
|
||||
];
|
||||
|
||||
echo json_encode($response);
|
||||
?>
|
||||
@@ -5,7 +5,8 @@
|
||||
"require": {
|
||||
"jumbojett/openid-connect-php": "^1.0.0",
|
||||
"phpseclib/phpseclib": "~3.0.7",
|
||||
"robthree/twofactorauth": "^1.7",
|
||||
"endroid/qr-code": "^4.0"
|
||||
"robthree/twofactorauth": "^3.0",
|
||||
"endroid/qr-code": "^5.0",
|
||||
"sabre/dav": "^4.4"
|
||||
}
|
||||
}
|
||||
573
composer.lock
generated
573
composer.lock
generated
@@ -4,32 +4,32 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c9857f23364f2280ef4b71cdc72d3f78",
|
||||
"content-hash": "3a9b8d9fcfdaaa865ba03eab392e88fd",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
"version": "2.0.8",
|
||||
"version": "v3.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Bacon/BaconQrCode.git",
|
||||
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
|
||||
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
|
||||
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
|
||||
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f",
|
||||
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dasprid/enum": "^1.0.3",
|
||||
"ext-iconv": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phly/keep-a-changelog": "^2.1",
|
||||
"phpunit/phpunit": "^7 | ^8 | ^9",
|
||||
"spatie/phpunit-snapshot-assertions": "^4.2.9",
|
||||
"squizlabs/php_codesniffer": "^3.4"
|
||||
"phly/keep-a-changelog": "^2.12",
|
||||
"phpunit/phpunit": "^10.5.11 || 11.0.4",
|
||||
"spatie/phpunit-snapshot-assertions": "^5.1.5",
|
||||
"squizlabs/php_codesniffer": "^3.9"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "to generate QR code images"
|
||||
@@ -56,9 +56,9 @@
|
||||
"homepage": "https://github.com/Bacon/BaconQrCode",
|
||||
"support": {
|
||||
"issues": "https://github.com/Bacon/BaconQrCode/issues",
|
||||
"source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
|
||||
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1"
|
||||
},
|
||||
"time": "2022-12-07T17:46:57+00:00"
|
||||
"time": "2024-10-01T13:55:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dasprid/enum",
|
||||
@@ -112,29 +112,26 @@
|
||||
},
|
||||
{
|
||||
"name": "endroid/qr-code",
|
||||
"version": "4.8.5",
|
||||
"version": "5.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/endroid/qr-code.git",
|
||||
"reference": "0db25b506a8411a5e1644ebaa67123a6eb7b6a77"
|
||||
"reference": "393fec6c4cbdc1bd65570ac9d245704428010122"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/endroid/qr-code/zipball/0db25b506a8411a5e1644ebaa67123a6eb7b6a77",
|
||||
"reference": "0db25b506a8411a5e1644ebaa67123a6eb7b6a77",
|
||||
"url": "https://api.github.com/repos/endroid/qr-code/zipball/393fec6c4cbdc1bd65570ac9d245704428010122",
|
||||
"reference": "393fec6c4cbdc1bd65570ac9d245704428010122",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"bacon/bacon-qr-code": "^2.0.5",
|
||||
"bacon/bacon-qr-code": "^3.0",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"conflict": {
|
||||
"khanamiryan/qrcode-detector-decoder": "^1.0.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"endroid/quality": "dev-master",
|
||||
"endroid/quality": "dev-main",
|
||||
"ext-gd": "*",
|
||||
"khanamiryan/qrcode-detector-decoder": "^1.0.4||^2.0.2",
|
||||
"khanamiryan/qrcode-detector-decoder": "^2.0.2",
|
||||
"setasign/fpdf": "^1.8.2"
|
||||
},
|
||||
"suggest": {
|
||||
@@ -146,7 +143,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.x-dev"
|
||||
"dev-main": "5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -175,7 +172,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/endroid/qr-code/issues",
|
||||
"source": "https://github.com/endroid/qr-code/tree/4.8.5"
|
||||
"source": "https://github.com/endroid/qr-code/tree/5.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -183,7 +180,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-09-29T14:03:20+00:00"
|
||||
"time": "2024-09-08T08:52:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jumbojett/openid-connect-php",
|
||||
@@ -455,25 +452,76 @@
|
||||
"time": "2024-12-14T21:12:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "robthree/twofactorauth",
|
||||
"version": "1.8.2",
|
||||
"name": "psr/log",
|
||||
"version": "3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/RobThree/TwoFactorAuth.git",
|
||||
"reference": "65681de5a324eae05140ac58b08648a60212afc0"
|
||||
"url": "https://github.com/php-fig/log.git",
|
||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/65681de5a324eae05140ac58b08648a60212afc0",
|
||||
"reference": "65681de5a324eae05140ac58b08648a60212afc0",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.6.0"
|
||||
"php": ">=8.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Log\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for logging libraries",
|
||||
"homepage": "https://github.com/php-fig/log",
|
||||
"keywords": [
|
||||
"log",
|
||||
"psr",
|
||||
"psr-3"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||
},
|
||||
"time": "2024-09-11T13:17:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "robthree/twofactorauth",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/RobThree/TwoFactorAuth.git",
|
||||
"reference": "6d70f9ca8e25568f163a7b3b3ff77bd8ea743978"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/6d70f9ca8e25568f163a7b3b3ff77bd8ea743978",
|
||||
"reference": "6d70f9ca8e25568f163a7b3b3ff77bd8ea743978",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpunit/phpunit": "@stable"
|
||||
"friendsofphp/php-cs-fixer": "^3.13",
|
||||
"phpstan/phpstan": "^1.9",
|
||||
"phpunit/phpunit": "^9"
|
||||
},
|
||||
"suggest": {
|
||||
"bacon/bacon-qr-code": "Needed for BaconQrCodeProvider provider",
|
||||
@@ -494,6 +542,16 @@
|
||||
"name": "Rob Janssen",
|
||||
"homepage": "http://robiii.me",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas CARPi",
|
||||
"homepage": "https://github.com/NicolasCARPi",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Will Power",
|
||||
"homepage": "https://github.com/willpower232",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Two Factor Authentication",
|
||||
@@ -522,7 +580,452 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2022-03-22T16:11:07+00:00"
|
||||
"time": "2024-10-24T15:14:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabre/dav",
|
||||
"version": "4.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/dav.git",
|
||||
"reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a",
|
||||
"reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-ctype": "*",
|
||||
"ext-date": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pcre": "*",
|
||||
"ext-simplexml": "*",
|
||||
"ext-spl": "*",
|
||||
"lib-libxml": ">=2.7.0",
|
||||
"php": "^7.1.0 || ^8.0",
|
||||
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||
"sabre/event": "^5.0",
|
||||
"sabre/http": "^5.0.5",
|
||||
"sabre/uri": "^2.0",
|
||||
"sabre/vobject": "^4.2.1",
|
||||
"sabre/xml": "^2.0.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.19",
|
||||
"monolog/monolog": "^1.27 || ^2.0",
|
||||
"phpstan/phpstan": "^0.12 || ^1.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-curl": "*",
|
||||
"ext-imap": "*",
|
||||
"ext-pdo": "*"
|
||||
},
|
||||
"bin": [
|
||||
"bin/sabredav",
|
||||
"bin/naturalselection"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Sabre\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "WebDAV Framework for PHP",
|
||||
"homepage": "http://sabre.io/",
|
||||
"keywords": [
|
||||
"CalDAV",
|
||||
"CardDAV",
|
||||
"WebDAV",
|
||||
"framework",
|
||||
"iCalendar"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"issues": "https://github.com/sabre-io/dav/issues",
|
||||
"source": "https://github.com/fruux/sabre-dav"
|
||||
},
|
||||
"time": "2024-10-29T11:46:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabre/event",
|
||||
"version": "5.1.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/event.git",
|
||||
"reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/event/zipball/86d57e305c272898ba3c28e9bd3d65d5464587c2",
|
||||
"reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "~2.17.1||^3.63",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/coroutine.php",
|
||||
"lib/Loop/functions.php",
|
||||
"lib/Promise/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Sabre\\Event\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "sabre/event is a library for lightweight event-based programming",
|
||||
"homepage": "http://sabre.io/event/",
|
||||
"keywords": [
|
||||
"EventEmitter",
|
||||
"async",
|
||||
"coroutine",
|
||||
"eventloop",
|
||||
"events",
|
||||
"hooks",
|
||||
"plugin",
|
||||
"promise",
|
||||
"reactor",
|
||||
"signal"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"issues": "https://github.com/sabre-io/event/issues",
|
||||
"source": "https://github.com/fruux/sabre-event"
|
||||
},
|
||||
"time": "2024-08-27T11:23:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabre/http",
|
||||
"version": "5.1.12",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/http.git",
|
||||
"reference": "dedff73f3995578bc942fa4c8484190cac14f139"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/http/zipball/dedff73f3995578bc942fa4c8484190cac14f139",
|
||||
"reference": "dedff73f3995578bc942fa4c8484190cac14f139",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-ctype": "*",
|
||||
"ext-curl": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0",
|
||||
"sabre/event": ">=4.0 <6.0",
|
||||
"sabre/uri": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "~2.17.1||^3.63",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-curl": " to make http requests with the Client class"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Sabre\\HTTP\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "The sabre/http library provides utilities for dealing with http requests and responses. ",
|
||||
"homepage": "https://github.com/fruux/sabre-http",
|
||||
"keywords": [
|
||||
"http"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"issues": "https://github.com/sabre-io/http/issues",
|
||||
"source": "https://github.com/fruux/sabre-http"
|
||||
},
|
||||
"time": "2024-08-27T16:07:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabre/uri",
|
||||
"version": "2.3.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/uri.git",
|
||||
"reference": "b76524c22de90d80ca73143680a8e77b1266c291"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291",
|
||||
"reference": "b76524c22de90d80ca73143680a8e77b1266c291",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.63",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"phpstan/phpstan-phpunit": "^1.4",
|
||||
"phpstan/phpstan-strict-rules": "^1.6",
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Sabre\\Uri\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Functions for making sense out of URIs.",
|
||||
"homepage": "http://sabre.io/uri/",
|
||||
"keywords": [
|
||||
"rfc3986",
|
||||
"uri",
|
||||
"url"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"issues": "https://github.com/sabre-io/uri/issues",
|
||||
"source": "https://github.com/fruux/sabre-uri"
|
||||
},
|
||||
"time": "2024-08-27T12:18:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabre/vobject",
|
||||
"version": "4.5.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/vobject.git",
|
||||
"reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/ff22611a53782e90c97be0d0bc4a5f98a5c0a12c",
|
||||
"reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0",
|
||||
"sabre/xml": "^2.1 || ^3.0 || ^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "~2.17.1",
|
||||
"phpstan/phpstan": "^0.12 || ^1.12 || ^2.0",
|
||||
"phpunit/php-invoker": "^2.0 || ^3.1",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||
},
|
||||
"suggest": {
|
||||
"hoa/bench": "If you would like to run the benchmark scripts"
|
||||
},
|
||||
"bin": [
|
||||
"bin/vobject",
|
||||
"bin/generate_vcards"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Sabre\\VObject\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Dominik Tobschall",
|
||||
"email": "dominik@fruux.com",
|
||||
"homepage": "http://tobschall.de/",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Ivan Enderlin",
|
||||
"email": "ivan.enderlin@hoa-project.net",
|
||||
"homepage": "http://mnt.io/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
|
||||
"homepage": "http://sabre.io/vobject/",
|
||||
"keywords": [
|
||||
"availability",
|
||||
"freebusy",
|
||||
"iCalendar",
|
||||
"ical",
|
||||
"ics",
|
||||
"jCal",
|
||||
"jCard",
|
||||
"recurrence",
|
||||
"rfc2425",
|
||||
"rfc2426",
|
||||
"rfc2739",
|
||||
"rfc4770",
|
||||
"rfc5545",
|
||||
"rfc5546",
|
||||
"rfc6321",
|
||||
"rfc6350",
|
||||
"rfc6351",
|
||||
"rfc6474",
|
||||
"rfc6638",
|
||||
"rfc6715",
|
||||
"rfc6868",
|
||||
"vCalendar",
|
||||
"vCard",
|
||||
"vcf",
|
||||
"xCal",
|
||||
"xCard"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"issues": "https://github.com/sabre-io/vobject/issues",
|
||||
"source": "https://github.com/fruux/sabre-vobject"
|
||||
},
|
||||
"time": "2025-04-17T09:22:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabre/xml",
|
||||
"version": "2.2.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/xml.git",
|
||||
"reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc",
|
||||
"reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-xmlreader": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"lib-libxml": ">=2.6.20",
|
||||
"php": "^7.1 || ^8.0",
|
||||
"sabre/uri": ">=1.0,<3.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "~2.17.1||3.63.2",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/Deserializer/functions.php",
|
||||
"lib/Serializer/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Sabre\\Xml\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Markus Staab",
|
||||
"email": "markus.staab@redaxo.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "sabre/xml is an XML library that you may not hate.",
|
||||
"homepage": "https://sabre.io/xml/",
|
||||
"keywords": [
|
||||
"XMLReader",
|
||||
"XMLWriter",
|
||||
"dom",
|
||||
"xml"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"issues": "https://github.com/sabre-io/xml/issues",
|
||||
"source": "https://github.com/fruux/sabre-xml"
|
||||
},
|
||||
"time": "2024-09-06T07:37:46+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
|
||||
145
config.php
145
config.php
@@ -1,145 +0,0 @@
|
||||
<?php
|
||||
// config.php
|
||||
|
||||
// Define constants.
|
||||
define('UPLOAD_DIR', '/var/www/uploads/');
|
||||
define('USERS_DIR', '/var/www/users/');
|
||||
define('USERS_FILE', 'users.txt');
|
||||
define('META_DIR', '/var/www/metadata/');
|
||||
define('META_FILE', 'file_metadata.json');
|
||||
define('TRASH_DIR', UPLOAD_DIR . 'trash/');
|
||||
define('TIMEZONE', 'America/New_York');
|
||||
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
|
||||
define('TOTAL_UPLOAD_SIZE', '5G');
|
||||
|
||||
date_default_timezone_set(TIMEZONE);
|
||||
|
||||
/**
|
||||
* Encrypts data using AES-256-CBC.
|
||||
*
|
||||
* @param string $data The plaintext.
|
||||
* @param string $encryptionKey The encryption key.
|
||||
* @return string Base64-encoded string containing IV and ciphertext.
|
||||
*/
|
||||
function encryptData($data, $encryptionKey)
|
||||
{
|
||||
$cipher = 'AES-256-CBC';
|
||||
$ivlen = openssl_cipher_iv_length($cipher);
|
||||
$iv = openssl_random_pseudo_bytes($ivlen);
|
||||
$ciphertext = openssl_encrypt($data, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||
return base64_encode($iv . $ciphertext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts data encrypted with AES-256-CBC.
|
||||
*
|
||||
* @param string $encryptedData Base64-encoded data containing IV and ciphertext.
|
||||
* @param string $encryptionKey The encryption key.
|
||||
* @return string|false The decrypted plaintext or false on failure.
|
||||
*/
|
||||
function decryptData($encryptedData, $encryptionKey)
|
||||
{
|
||||
$cipher = 'AES-256-CBC';
|
||||
$data = base64_decode($encryptedData);
|
||||
$ivlen = openssl_cipher_iv_length($cipher);
|
||||
$iv = substr($data, 0, $ivlen);
|
||||
$ciphertext = substr($data, $ivlen);
|
||||
return openssl_decrypt($ciphertext, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||
}
|
||||
|
||||
// Load encryption key from environment (override in production).
|
||||
$encryptionKey = getenv('PERSISTENT_TOKENS_KEY') ?: 'default_please_change_this_key';
|
||||
if (!$encryptionKey) {
|
||||
die('Encryption key for persistent tokens is not set.');
|
||||
}
|
||||
|
||||
function loadUserPermissions($username)
|
||||
{
|
||||
global $encryptionKey;
|
||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||
|
||||
if (file_exists($permissionsFile)) {
|
||||
$content = file_get_contents($permissionsFile);
|
||||
|
||||
// Try to decrypt the content.
|
||||
$decryptedContent = decryptData($content, $encryptionKey);
|
||||
if ($decryptedContent !== false) {
|
||||
$permissions = json_decode($decryptedContent, true);
|
||||
} else {
|
||||
$permissions = json_decode($content, true);
|
||||
}
|
||||
|
||||
if (is_array($permissions) && array_key_exists($username, $permissions)) {
|
||||
$result = $permissions[$username];
|
||||
return !empty($result) ? $result : false;
|
||||
}
|
||||
}
|
||||
// Removed error_log() to prevent flooding logs when file is not found.
|
||||
return false; // Return false if no permissions found.
|
||||
}
|
||||
|
||||
// Determine whether HTTPS is used.
|
||||
$envSecure = getenv('SECURE');
|
||||
if ($envSecure !== false) {
|
||||
$secure = filter_var($envSecure, FILTER_VALIDATE_BOOLEAN);
|
||||
} else {
|
||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
}
|
||||
|
||||
$cookieParams = [
|
||||
'lifetime' => 7200,
|
||||
'path' => '/',
|
||||
'domain' => '', // Set your domain as needed.
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
];
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_set_cookie_params($cookieParams);
|
||||
ini_set('session.gc_maxlifetime', 7200);
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
// Auto-login via persistent token.
|
||||
if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token'])) {
|
||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
||||
$persistentTokens = [];
|
||||
if (file_exists($persistentTokensFile)) {
|
||||
$encryptedContent = file_get_contents($persistentTokensFile);
|
||||
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
||||
$persistentTokens = json_decode($decryptedContent, true);
|
||||
if (!is_array($persistentTokens)) {
|
||||
$persistentTokens = [];
|
||||
}
|
||||
}
|
||||
if (isset($persistentTokens[$_COOKIE['remember_me_token']])) {
|
||||
$tokenData = $persistentTokens[$_COOKIE['remember_me_token']];
|
||||
if ($tokenData['expiry'] >= time()) {
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $tokenData["username"];
|
||||
// IMPORTANT: Set the folderOnly flag here for auto-login.
|
||||
$_SESSION["folderOnly"] = loadUserPermissions($tokenData["username"]);
|
||||
} else {
|
||||
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
||||
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||
file_put_contents($persistentTokensFile, $newEncryptedContent, LOCK_EX);
|
||||
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
define('BASE_URL', 'http://yourwebsite/uploads/');
|
||||
|
||||
if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||||
$defaultShareUrl = isset($_SERVER['HTTP_HOST'])
|
||||
? "http://" . $_SERVER['HTTP_HOST'] . "/share.php"
|
||||
: "http://localhost/share.php";
|
||||
} else {
|
||||
$defaultShareUrl = rtrim(BASE_URL, '/') . "/share.php";
|
||||
}
|
||||
define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl);
|
||||
208
config/config.php
Normal file
208
config/config.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
// config.php
|
||||
|
||||
// Prevent caching
|
||||
header("Cache-Control: no-cache, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Expires: 0");
|
||||
|
||||
// Security headers
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header("X-Frame-Options: SAMEORIGIN");
|
||||
header("Referrer-Policy: no-referrer-when-downgrade");
|
||||
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
|
||||
header("X-XSS-Protection: 1; mode=block");
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||
header("Strict-Transport-Security: max-age=31536000; includeSubDomains; preload");
|
||||
}
|
||||
|
||||
// Define constants
|
||||
define('PROJECT_ROOT', dirname(__DIR__));
|
||||
define('UPLOAD_DIR', '/var/www/uploads/');
|
||||
define('USERS_DIR', '/var/www/users/');
|
||||
define('USERS_FILE', 'users.txt');
|
||||
define('META_DIR', '/var/www/metadata/');
|
||||
define('META_FILE', 'file_metadata.json');
|
||||
define('TRASH_DIR', UPLOAD_DIR . 'trash/');
|
||||
define('TIMEZONE', 'America/New_York');
|
||||
define('DATE_TIME_FORMAT','m/d/y h:iA');
|
||||
define('TOTAL_UPLOAD_SIZE','5G');
|
||||
define('REGEX_FOLDER_NAME', '/^[\p{L}\p{N}_\-\s\/\\\\]+$/u');
|
||||
define('PATTERN_FOLDER_NAME','[\p{L}\p{N}_\-\s\/\\\\]+');
|
||||
define('REGEX_FILE_NAME', '/^[^\x00-\x1F\/\\\\]{1,255}$/u');
|
||||
define('REGEX_USER', '/^[\p{L}\p{N}_\- ]+$/u');
|
||||
|
||||
date_default_timezone_set(TIMEZONE);
|
||||
|
||||
|
||||
// Encryption helpers
|
||||
function encryptData($data, $encryptionKey)
|
||||
{
|
||||
$cipher = 'AES-256-CBC';
|
||||
$ivlen = openssl_cipher_iv_length($cipher);
|
||||
$iv = openssl_random_pseudo_bytes($ivlen);
|
||||
$ct = openssl_encrypt($data, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||
return base64_encode($iv . $ct);
|
||||
}
|
||||
|
||||
function decryptData($encryptedData, $encryptionKey)
|
||||
{
|
||||
$cipher = 'AES-256-CBC';
|
||||
$data = base64_decode($encryptedData);
|
||||
$ivlen = openssl_cipher_iv_length($cipher);
|
||||
$iv = substr($data, 0, $ivlen);
|
||||
$ct = substr($data, $ivlen);
|
||||
return openssl_decrypt($ct, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||
}
|
||||
|
||||
// Load encryption key
|
||||
$envKey = getenv('PERSISTENT_TOKENS_KEY');
|
||||
if ($envKey === false || $envKey === '') {
|
||||
$encryptionKey = 'default_please_change_this_key';
|
||||
error_log('WARNING: Using default encryption key. Please set PERSISTENT_TOKENS_KEY in your environment.');
|
||||
} else {
|
||||
$encryptionKey = $envKey;
|
||||
}
|
||||
|
||||
// Helper to load JSON permissions (with optional decryption)
|
||||
function loadUserPermissions($username)
|
||||
{
|
||||
global $encryptionKey;
|
||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||
if (file_exists($permissionsFile)) {
|
||||
$content = file_get_contents($permissionsFile);
|
||||
$decrypted = decryptData($content, $encryptionKey);
|
||||
$json = ($decrypted !== false) ? $decrypted : $content;
|
||||
$perms = json_decode($json, true);
|
||||
if (is_array($perms) && isset($perms[$username])) {
|
||||
return !empty($perms[$username]) ? $perms[$username] : false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine HTTPS usage
|
||||
$envSecure = getenv('SECURE');
|
||||
$secure = ($envSecure !== false)
|
||||
? filter_var($envSecure, FILTER_VALIDATE_BOOLEAN)
|
||||
: (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
|
||||
// Choose session lifetime based on "remember me" cookie
|
||||
$defaultSession = 7200; // 2 hours
|
||||
$persistentDays = 30 * 24 * 60 * 60; // 30 days
|
||||
$sessionLifetime = isset($_COOKIE['remember_me_token'])
|
||||
? $persistentDays
|
||||
: $defaultSession;
|
||||
|
||||
// Configure PHP session cookie and GC
|
||||
session_set_cookie_params([
|
||||
'lifetime' => $sessionLifetime,
|
||||
'path' => '/',
|
||||
'domain' => '', // adjust if you need a specific domain
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
]);
|
||||
ini_set('session.gc_maxlifetime', (string)$sessionLifetime);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// CSRF token
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
|
||||
// Auto‑login via persistent token
|
||||
if (empty($_SESSION["authenticated"]) && !empty($_COOKIE['remember_me_token'])) {
|
||||
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||
$tokens = [];
|
||||
if (file_exists($tokFile)) {
|
||||
$enc = file_get_contents($tokFile);
|
||||
$dec = decryptData($enc, $encryptionKey);
|
||||
$tokens = json_decode($dec, true) ?: [];
|
||||
}
|
||||
$token = $_COOKIE['remember_me_token'];
|
||||
if (!empty($tokens[$token])) {
|
||||
$data = $tokens[$token];
|
||||
if ($data['expiry'] >= time()) {
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $data["username"];
|
||||
$_SESSION["folderOnly"] = loadUserPermissions($data["username"]);
|
||||
$_SESSION["isAdmin"] = !empty($data["isAdmin"]);
|
||||
} else {
|
||||
// expired — clean up
|
||||
unset($tokens[$token]);
|
||||
file_put_contents($tokFile, encryptData(json_encode($tokens, JSON_PRETTY_PRINT), $encryptionKey), LOCK_EX);
|
||||
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
||||
|
||||
// sane defaults:
|
||||
$cfgAuthBypass = false;
|
||||
$cfgAuthHeader = 'X_REMOTE_USER';
|
||||
|
||||
if (file_exists($adminConfigFile)) {
|
||||
$encrypted = file_get_contents($adminConfigFile);
|
||||
$decrypted = decryptData($encrypted, $encryptionKey);
|
||||
$adminCfg = json_decode($decrypted, true) ?: [];
|
||||
|
||||
$loginOpts = $adminCfg['loginOptions'] ?? [];
|
||||
|
||||
// proxy-only bypass flag
|
||||
$cfgAuthBypass = ! empty($loginOpts['authBypass']);
|
||||
|
||||
// header name (e.g. “X-Remote-User” → HTTP_X_REMOTE_USER)
|
||||
$hdr = trim($loginOpts['authHeaderName'] ?? '');
|
||||
if ($hdr === '') {
|
||||
$hdr = 'X-Remote-User';
|
||||
}
|
||||
// normalize to PHP’s $_SERVER key format:
|
||||
$cfgAuthHeader = 'HTTP_' . strtoupper(str_replace('-', '_', $hdr));
|
||||
}
|
||||
|
||||
define('AUTH_BYPASS', $cfgAuthBypass);
|
||||
define('AUTH_HEADER', $cfgAuthHeader);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// PROXY-ONLY AUTO–LOGIN now uses those constants:
|
||||
if (AUTH_BYPASS) {
|
||||
$hdrKey = AUTH_HEADER; // e.g. "HTTP_X_REMOTE_USER"
|
||||
if (!empty($_SERVER[$hdrKey])) {
|
||||
// regenerate once per session
|
||||
if (empty($_SESSION['authenticated'])) {
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
|
||||
$username = $_SERVER[$hdrKey];
|
||||
$_SESSION['authenticated'] = true;
|
||||
$_SESSION['username'] = $username;
|
||||
|
||||
// ◾ lookup actual role instead of forcing admin
|
||||
require_once PROJECT_ROOT . '/src/models/AuthModel.php';
|
||||
$role = AuthModel::getUserRole($username);
|
||||
$_SESSION['isAdmin'] = ($role === '1');
|
||||
|
||||
// carry over any folder/read/upload perms
|
||||
$perms = loadUserPermissions($username) ?: [];
|
||||
$_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
|
||||
$_SESSION['readOnly'] = $perms['readOnly'] ?? false;
|
||||
$_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
|
||||
}
|
||||
}
|
||||
// Share URL fallback
|
||||
define('BASE_URL', 'http://yourwebsite/uploads/');
|
||||
if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||||
$defaultShare = isset($_SERVER['HTTP_HOST'])
|
||||
? "http://{$_SERVER['HTTP_HOST']}/api/file/share.php"
|
||||
: "http://localhost/api/file/share.php";
|
||||
} else {
|
||||
$defaultShare = rtrim(BASE_URL, '/') . "/api/file/share.php";
|
||||
}
|
||||
define('SHARE_URL', getenv('SHARE_URL') ?: $defaultShare);
|
||||
153
copyFiles.php
153
copyFiles.php
@@ -1,153 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to copy files."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (
|
||||
!$data ||
|
||||
!isset($data['source']) ||
|
||||
!isset($data['destination']) ||
|
||||
!isset($data['files'])
|
||||
) {
|
||||
echo json_encode(["error" => "Invalid request"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sourceFolder = trim($data['source']);
|
||||
$destinationFolder = trim($data['destination']);
|
||||
$files = $data['files'];
|
||||
|
||||
// Validate folder names: allow letters, numbers, underscores, dashes, spaces, and forward slashes.
|
||||
$folderPattern = '/^[A-Za-z0-9_\- \/]+$/';
|
||||
if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) {
|
||||
echo json_encode(["error" => "Invalid source folder name."]);
|
||||
exit;
|
||||
}
|
||||
if ($destinationFolder !== 'root' && !preg_match($folderPattern, $destinationFolder)) {
|
||||
echo json_encode(["error" => "Invalid destination folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Trim any leading/trailing slashes and spaces.
|
||||
$sourceFolder = trim($sourceFolder, "/\\ ");
|
||||
$destinationFolder = trim($destinationFolder, "/\\ ");
|
||||
|
||||
// Build the source and destination directories.
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$sourceDir = ($sourceFolder === 'root')
|
||||
? $baseDir . DIRECTORY_SEPARATOR
|
||||
: $baseDir . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR;
|
||||
$destDir = ($destinationFolder === 'root')
|
||||
? $baseDir . DIRECTORY_SEPARATOR
|
||||
: $baseDir . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR;
|
||||
|
||||
// Helper: Generate the metadata file path for a given folder.
|
||||
function getMetadataFilePath($folder) {
|
||||
if (strtolower($folder) === 'root' || $folder === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
|
||||
}
|
||||
|
||||
// Helper: Generate a unique file name if a file with the same name exists.
|
||||
function getUniqueFileName($destDir, $fileName) {
|
||||
$fullPath = $destDir . $fileName;
|
||||
clearstatcache(true, $fullPath);
|
||||
if (!file_exists($fullPath)) {
|
||||
return $fileName;
|
||||
}
|
||||
$basename = pathinfo($fileName, PATHINFO_FILENAME);
|
||||
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
|
||||
$counter = 1;
|
||||
do {
|
||||
$newName = $basename . " (" . $counter . ")" . ($extension ? "." . $extension : "");
|
||||
$newFullPath = $destDir . $newName;
|
||||
clearstatcache(true, $newFullPath);
|
||||
$counter++;
|
||||
} while (file_exists($destDir . $newName));
|
||||
return $newName;
|
||||
}
|
||||
|
||||
// Load source and destination metadata.
|
||||
$srcMetaFile = getMetadataFilePath($sourceFolder);
|
||||
$destMetaFile = getMetadataFilePath($destinationFolder);
|
||||
|
||||
$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : [];
|
||||
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
|
||||
|
||||
$errors = [];
|
||||
|
||||
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
|
||||
foreach ($files as $fileName) {
|
||||
// Save the original name for metadata lookup.
|
||||
$originalName = basename(trim($fileName));
|
||||
$basename = $originalName;
|
||||
if (!preg_match($safeFileNamePattern, $basename)) {
|
||||
$errors[] = "$basename has an invalid name.";
|
||||
continue;
|
||||
}
|
||||
|
||||
$srcPath = $sourceDir . $originalName;
|
||||
$destPath = $destDir . $basename;
|
||||
|
||||
clearstatcache();
|
||||
if (!file_exists($srcPath)) {
|
||||
$errors[] = "$originalName does not exist in source.";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file_exists($destPath)) {
|
||||
$uniqueName = getUniqueFileName($destDir, $basename);
|
||||
$basename = $uniqueName; // update the file name for metadata and destination path
|
||||
$destPath = $destDir . $uniqueName;
|
||||
}
|
||||
|
||||
if (!copy($srcPath, $destPath)) {
|
||||
$errors[] = "Failed to copy $basename";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update destination metadata: if there's metadata for the original file in source, add it under the new name.
|
||||
if (isset($srcMetadata[$originalName])) {
|
||||
$destMetadata[$basename] = $srcMetadata[$originalName];
|
||||
}
|
||||
}
|
||||
|
||||
if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) {
|
||||
$errors[] = "Failed to update destination metadata.";
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
echo json_encode(["success" => "Files copied successfully"]);
|
||||
} else {
|
||||
echo json_encode(["error" => implode("; ", $errors)]);
|
||||
}
|
||||
?>
|
||||
@@ -1,96 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure the request is a POST
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid request method.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to create folders."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Get the JSON input and decode it
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!isset($input['folderName'])) {
|
||||
echo json_encode(['success' => false, 'error' => 'Folder name not provided.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folderName = trim($input['folderName']);
|
||||
$parent = isset($input['parent']) ? trim($input['parent']) : "";
|
||||
|
||||
// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces in folderName
|
||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $folderName)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid folder name.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Optionally, sanitize the parent folder if needed.
|
||||
if ($parent && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $parent)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid parent folder name.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Build the full folder path.
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
if ($parent && strtolower($parent) !== "root") {
|
||||
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $parent . DIRECTORY_SEPARATOR . $folderName;
|
||||
$relativePath = $parent . "/" . $folderName;
|
||||
} else {
|
||||
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $folderName;
|
||||
$relativePath = $folderName;
|
||||
}
|
||||
|
||||
// Check if the folder already exists.
|
||||
if (file_exists($fullPath)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Folder already exists.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Attempt to create the folder.
|
||||
if (mkdir($fullPath, 0755, true)) {
|
||||
|
||||
// --- Create an empty metadata file for the new folder ---
|
||||
// Helper: Generate the metadata file path for a given folder.
|
||||
// For "root", returns "root_metadata.json". Otherwise, replaces slashes, backslashes, and spaces with dashes and appends "_metadata.json".
|
||||
function getMetadataFilePath($folder) {
|
||||
if (strtolower($folder) === 'root' || $folder === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
|
||||
}
|
||||
|
||||
$metadataFile = getMetadataFilePath($relativePath);
|
||||
// Create an empty associative array (i.e. empty metadata) and write to the metadata file.
|
||||
file_put_contents($metadataFile, json_encode([], JSON_PRETTY_PRINT));
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to create folder.']);
|
||||
}
|
||||
?>
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
// createFolderShareLink.php
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
// Get POST input.
|
||||
$input = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$input) {
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = isset($input['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;
|
||||
|
||||
// Validate folder name using regex.
|
||||
// Allow letters, numbers, underscores, hyphens, spaces and slashes.
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Generate a secure token.
|
||||
try {
|
||||
$token = bin2hex(random_bytes(16)); // 32 hex characters.
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(["error" => "Could not generate token."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Calculate expiration time (Unix timestamp).
|
||||
$expires = time() + ($expirationMinutes * 60);
|
||||
|
||||
// Hash password if provided.
|
||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
|
||||
// Define the file to store share folder links.
|
||||
$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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up expired share links.
|
||||
$currentTime = time();
|
||||
foreach ($shareLinks as $key => $link) {
|
||||
if (isset($link["expires"]) && $link["expires"] < $currentTime) {
|
||||
unset($shareLinks[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new share record.
|
||||
$shareLinks[$token] = [
|
||||
"folder" => $folder,
|
||||
"expires" => $expires,
|
||||
"password" => $hashedPassword,
|
||||
"allowUpload" => $allowUpload
|
||||
];
|
||||
|
||||
// Save the share links.
|
||||
if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT))) {
|
||||
// Determine base URL.
|
||||
if (defined('BASE_URL') && !empty(BASE_URL) && strpos(BASE_URL, 'yourwebsite') === false) {
|
||||
$baseUrl = rtrim(BASE_URL, '/');
|
||||
} else {
|
||||
// Prefer HTTP_HOST over SERVER_ADDR.
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||
// Use HTTP_HOST if set; fallback to gethostbyname if needed.
|
||||
$host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : gethostbyname($_SERVER['SERVER_ADDR'] ?? 'localhost');
|
||||
$baseUrl = $protocol . "://" . $host;
|
||||
}
|
||||
// The share URL points to shareFolder.php.
|
||||
$link = $baseUrl . "/shareFolder.php?token=" . urlencode($token);
|
||||
echo json_encode(["token" => $token, "expires" => $expires, "link" => $link]);
|
||||
} else {
|
||||
echo json_encode(["error" => "Could not save share link."]);
|
||||
}
|
||||
?>
|
||||
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
// createShareLink.php
|
||||
require_once 'config.php';
|
||||
|
||||
// Get POST input.
|
||||
$input = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$input) {
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = isset($input['folder']) ? trim($input['folder']) : "";
|
||||
$file = isset($input['file']) ? basename($input['file']) : "";
|
||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
||||
$password = isset($input['password']) ? $input['password'] : "";
|
||||
|
||||
// Validate folder using regex.
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Generate a secure token.
|
||||
$token = bin2hex(random_bytes(16)); // 32 hex characters.
|
||||
|
||||
// Calculate expiration (Unix timestamp).
|
||||
$expires = time() + ($expirationMinutes * 60);
|
||||
|
||||
// Hash password if provided.
|
||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
|
||||
// File to store share links.
|
||||
$shareFile = META_DIR . "share_links.json";
|
||||
$shareLinks = [];
|
||||
if (file_exists($shareFile)) {
|
||||
$data = file_get_contents($shareFile);
|
||||
$shareLinks = json_decode($data, true);
|
||||
if (!is_array($shareLinks)) {
|
||||
$shareLinks = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up expired share links.
|
||||
$currentTime = time();
|
||||
foreach ($shareLinks as $key => $link) {
|
||||
if ($link["expires"] < $currentTime) {
|
||||
unset($shareLinks[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add record.
|
||||
$shareLinks[$token] = [
|
||||
"folder" => $folder,
|
||||
"file" => $file,
|
||||
"expires" => $expires,
|
||||
"password" => $hashedPassword
|
||||
];
|
||||
|
||||
// Save the share links.
|
||||
if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT))) {
|
||||
echo json_encode(["token" => $token, "expires" => $expires]);
|
||||
} else {
|
||||
echo json_encode(["error" => "Could not save share link."]);
|
||||
}
|
||||
?>
|
||||
53
custom-php.ini
Normal file
53
custom-php.ini
Normal file
@@ -0,0 +1,53 @@
|
||||
; custom-php.ini
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; OPcache Settings
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
opcache.enable=1
|
||||
opcache.enable_cli=0
|
||||
; Allocate 128MB of memory for opcode caching
|
||||
opcache.memory_consumption=128
|
||||
; Increase the maximum number of accelerated files (adjust if you have a large codebase)
|
||||
opcache.max_accelerated_files=4000
|
||||
; Refresh file timestamp every 60 seconds to avoid too many disk reads
|
||||
opcache.revalidate_freq=60
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Memory and Execution Time Limits
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Increase memory limit to 512M for large file processing or image processing operations
|
||||
memory_limit=512M
|
||||
; Set execution time limits to accommodate long-running uploads/processes
|
||||
max_execution_time=300
|
||||
max_input_time=300
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Realpath Cache Settings
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
realpath_cache_size=4096k
|
||||
realpath_cache_ttl=600
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; File Upload Settings
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Allow a maximum of 20 files per request
|
||||
max_file_uploads=20
|
||||
; Ensure the temporary directory is set (should exist and be writable)
|
||||
upload_tmp_dir=/tmp
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Session Configuration (if applicable)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
session.gc_maxlifetime=1440
|
||||
session.gc_probability=1
|
||||
session.gc_divisor=100
|
||||
session.save_path = "/var/www/sessions"
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Error Handling / Logging
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Do not display errors publicly in production
|
||||
display_errors=Off
|
||||
; Log errors to a dedicated file
|
||||
log_errors=On
|
||||
error_log=/var/log/php8.3-error.log
|
||||
161
deleteFiles.php
161
deleteFiles.php
@@ -1,161 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Define $username first.
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
|
||||
// Now load the user's permissions.
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
|
||||
// Check if the user is read-only.
|
||||
if ($username) {
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to delete files."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Setup Trash Folder & Metadata ---
|
||||
$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||
if (!file_exists($trashDir)) {
|
||||
mkdir($trashDir, 0755, true);
|
||||
}
|
||||
$trashMetadataFile = $trashDir . "trash.json";
|
||||
$trashData = [];
|
||||
if (file_exists($trashMetadataFile)) {
|
||||
$json = file_get_contents($trashMetadataFile);
|
||||
$trashData = json_decode($json, true);
|
||||
if (!is_array($trashData)) {
|
||||
$trashData = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Generate the metadata file path for a given folder.
|
||||
function getMetadataFilePath($folder) {
|
||||
if (strtolower($folder) === 'root' || $folder === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
|
||||
}
|
||||
|
||||
// Read request body
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
// Validate request
|
||||
if (!isset($data['files']) || !is_array($data['files'])) {
|
||||
echo json_encode(["error" => "No file names provided"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Determine folder – default to 'root'
|
||||
$folder = isset($data['folder']) ? trim($data['folder']) : 'root';
|
||||
|
||||
// Validate folder: allow letters, numbers, underscores, dashes, spaces, and forward slashes
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
$folder = trim($folder, "/\\ ");
|
||||
|
||||
// Build the upload directory.
|
||||
if ($folder !== 'root') {
|
||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
|
||||
} else {
|
||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
// Load folder metadata (if exists) to retrieve uploader and upload date.
|
||||
$metadataFile = getMetadataFilePath($folder);
|
||||
$folderMetadata = [];
|
||||
if (file_exists($metadataFile)) {
|
||||
$folderMetadata = json_decode(file_get_contents($metadataFile), true);
|
||||
if (!is_array($folderMetadata)) {
|
||||
$folderMetadata = [];
|
||||
}
|
||||
}
|
||||
|
||||
$movedFiles = [];
|
||||
$errors = [];
|
||||
|
||||
// Define a safe file name pattern: allow letters, numbers, underscores, dashes, dots, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
|
||||
foreach ($data['files'] as $fileName) {
|
||||
$basename = basename(trim($fileName));
|
||||
|
||||
// Validate the file name.
|
||||
if (!preg_match($safeFileNamePattern, $basename)) {
|
||||
$errors[] = "$basename has an invalid name.";
|
||||
continue;
|
||||
}
|
||||
|
||||
$filePath = $uploadDir . $basename;
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
// Append a timestamp to the file name in trash to avoid collisions.
|
||||
$timestamp = time();
|
||||
$trashFileName = $basename . "_" . $timestamp;
|
||||
if (rename($filePath, $trashDir . $trashFileName)) {
|
||||
$movedFiles[] = $basename;
|
||||
// Record trash metadata for possible restoration.
|
||||
$trashData[] = [
|
||||
'type' => 'file',
|
||||
'originalFolder' => $uploadDir, // You could also store a relative path here.
|
||||
'originalName' => $basename,
|
||||
'trashName' => $trashFileName,
|
||||
'trashedAt' => $timestamp,
|
||||
// Enrich trash record with uploader and upload date from folder metadata (if available)
|
||||
'uploaded' => isset($folderMetadata[$basename]['uploaded']) ? $folderMetadata[$basename]['uploaded'] : "Unknown",
|
||||
'uploader' => isset($folderMetadata[$basename]['uploader']) ? $folderMetadata[$basename]['uploader'] : "Unknown",
|
||||
// NEW: Record the username of the user who deleted the file.
|
||||
'deletedBy' => isset($_SESSION['username']) ? $_SESSION['username'] : "Unknown"
|
||||
];
|
||||
} else {
|
||||
$errors[] = "Failed to move $basename to Trash.";
|
||||
}
|
||||
} else {
|
||||
// Consider file already deleted.
|
||||
$movedFiles[] = $basename;
|
||||
}
|
||||
}
|
||||
|
||||
// Write back the updated trash metadata.
|
||||
file_put_contents($trashMetadataFile, json_encode($trashData, JSON_PRETTY_PRINT));
|
||||
|
||||
// Update folder-specific metadata file by removing deleted files.
|
||||
if (file_exists($metadataFile)) {
|
||||
$metadata = json_decode(file_get_contents($metadataFile), true);
|
||||
if (is_array($metadata)) {
|
||||
foreach ($movedFiles as $delFile) {
|
||||
if (isset($metadata[$delFile])) {
|
||||
unset($metadata[$delFile]);
|
||||
}
|
||||
}
|
||||
file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
echo json_encode(["success" => "Files moved to Trash: " . implode(", ", $movedFiles)]);
|
||||
} else {
|
||||
echo json_encode(["error" => implode("; ", $errors) . ". Files moved to Trash: " . implode(", ", $movedFiles)]);
|
||||
}
|
||||
?>
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure the request is a POST
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid request method.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to delete folders."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Get the JSON input and decode it
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!isset($input['folder'])) {
|
||||
echo json_encode(['success' => false, 'error' => 'Folder name not provided.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folderName = trim($input['folder']);
|
||||
|
||||
// Prevent deletion of root.
|
||||
if ($folderName === 'root') {
|
||||
echo json_encode(['success' => false, 'error' => 'Cannot delete root folder.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Allow letters, numbers, underscores, dashes, spaces, and forward slashes.
|
||||
if (!preg_match('/^[A-Za-z0-9_\- \/]+$/', $folderName)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid folder name.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Build the folder path (supports subfolder paths like "FolderTest/FolderTestSub")
|
||||
$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folderName;
|
||||
|
||||
// Check if the folder exists and is a directory
|
||||
if (!file_exists($folderPath) || !is_dir($folderPath)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Folder does not exist.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Prevent deletion if the folder is not empty
|
||||
if (count(scandir($folderPath)) > 2) {
|
||||
echo json_encode(['success' => false, 'error' => 'Folder is not empty.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Generate the metadata file path for a given folder.
|
||||
* For "root", returns "root_metadata.json". Otherwise, it replaces
|
||||
* slashes, backslashes, and spaces with dashes and appends "_metadata.json".
|
||||
*
|
||||
* @param string $folder The folder's relative path.
|
||||
* @return string The full path to the folder's metadata file.
|
||||
*/
|
||||
function getMetadataFilePath($folder) {
|
||||
if (strtolower($folder) === 'root' || $folder === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
|
||||
}
|
||||
|
||||
// Attempt to delete the folder.
|
||||
if (rmdir($folderPath)) {
|
||||
// Remove corresponding metadata file if it exists.
|
||||
$metadataFile = getMetadataFilePath($folderName);
|
||||
if (file_exists($metadataFile)) {
|
||||
unlink($metadataFile);
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to delete folder.']);
|
||||
}
|
||||
?>
|
||||
@@ -1,104 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Setup Trash Folder & Metadata ---
|
||||
$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||
if (!file_exists($trashDir)) {
|
||||
mkdir($trashDir, 0755, true);
|
||||
}
|
||||
$trashMetadataFile = $trashDir . "trash.json";
|
||||
|
||||
// Load trash metadata into an associative array keyed by trashName.
|
||||
$trashData = [];
|
||||
if (file_exists($trashMetadataFile)) {
|
||||
$json = file_get_contents($trashMetadataFile);
|
||||
$tempData = json_decode($json, true);
|
||||
if (is_array($tempData)) {
|
||||
foreach ($tempData as $item) {
|
||||
if (isset($item['trashName'])) {
|
||||
$trashData[$item['trashName']] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read request body.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$data) {
|
||||
echo json_encode(["error" => "Invalid input"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Determine deletion mode: if "deleteAll" is true, delete all trash items; otherwise, use provided "files" array.
|
||||
$filesToDelete = [];
|
||||
if (isset($data['deleteAll']) && $data['deleteAll'] === true) {
|
||||
$filesToDelete = array_keys($trashData);
|
||||
} elseif (isset($data['files']) && is_array($data['files'])) {
|
||||
$filesToDelete = $data['files'];
|
||||
} else {
|
||||
echo json_encode(["error" => "No trash file identifiers provided"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$deletedFiles = [];
|
||||
$errors = [];
|
||||
|
||||
// Define a safe file name pattern.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
|
||||
foreach ($filesToDelete as $trashName) {
|
||||
$trashName = trim($trashName);
|
||||
if (!preg_match($safeFileNamePattern, $trashName)) {
|
||||
$errors[] = "$trashName has an invalid format.";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($trashData[$trashName])) {
|
||||
$errors[] = "Trash item $trashName not found.";
|
||||
continue;
|
||||
}
|
||||
|
||||
$filePath = $trashDir . $trashName;
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
if (unlink($filePath)) {
|
||||
$deletedFiles[] = $trashName;
|
||||
unset($trashData[$trashName]);
|
||||
} else {
|
||||
$errors[] = "Failed to delete $trashName.";
|
||||
}
|
||||
} else {
|
||||
// If the file doesn't exist, remove its metadata entry.
|
||||
unset($trashData[$trashName]);
|
||||
$deletedFiles[] = $trashName;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the updated trash metadata back (as an indexed array).
|
||||
file_put_contents($trashMetadataFile, json_encode(array_values($trashData), JSON_PRETTY_PRINT));
|
||||
|
||||
if (empty($errors)) {
|
||||
echo json_encode(["success" => "Trash items deleted: " . implode(", ", $deletedFiles)]);
|
||||
} else {
|
||||
echo json_encode(["error" => implode("; ", $errors) . ". Trash items deleted: " . implode(", ", $deletedFiles)]);
|
||||
}
|
||||
exit;
|
||||
?>
|
||||
89
download.php
89
download.php
@@ -1,89 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
// Check if the user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get file parameters from the GET request.
|
||||
$file = isset($_GET['file']) ? basename($_GET['file']) : '';
|
||||
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
|
||||
|
||||
// Validate file name (allowing letters, numbers, underscores, dashes, dots, and parentheses)
|
||||
if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $file)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid file name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get the realpath of the upload directory.
|
||||
$uploadDirReal = realpath(UPLOAD_DIR);
|
||||
if ($uploadDirReal === false) {
|
||||
http_response_code(500);
|
||||
echo json_encode(["error" => "Server misconfiguration."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Determine the directory.
|
||||
if ($folder === 'root') {
|
||||
$directory = $uploadDirReal;
|
||||
} else {
|
||||
// Prevent path traversal in folder parameter.
|
||||
if (strpos($folder, '..') !== false) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$directoryPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
|
||||
$directory = realpath($directoryPath);
|
||||
|
||||
// Ensure that the resolved directory exists and is within the allowed UPLOAD_DIR.
|
||||
if ($directory === false || strpos($directory, $uploadDirReal) !== 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder path."]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the file path.
|
||||
$filePath = $directory . DIRECTORY_SEPARATOR . $file;
|
||||
$realFilePath = realpath($filePath);
|
||||
|
||||
// Validate that the real file path exists and is within the allowed directory.
|
||||
if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Access forbidden."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!file_exists($realFilePath)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(["error" => "File not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Serve the file.
|
||||
$mimeType = mime_content_type($realFilePath);
|
||||
header("Content-Type: " . $mimeType);
|
||||
|
||||
// For images, serve inline; for other types, force download.
|
||||
$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
|
||||
header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"');
|
||||
} else {
|
||||
header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
|
||||
}
|
||||
header('Content-Length: ' . filesize($realFilePath));
|
||||
|
||||
// Disable caching.
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
readfile($realFilePath);
|
||||
exit;
|
||||
?>
|
||||
@@ -1,89 +0,0 @@
|
||||
<?php
|
||||
// downloadSharedFile.php
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
// Retrieve and sanitize token and file name from GET.
|
||||
$token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING);
|
||||
$file = filter_input(INPUT_GET, 'file', FILTER_SANITIZE_STRING);
|
||||
|
||||
if (empty($token) || empty($file)) {
|
||||
http_response_code(400);
|
||||
echo "Missing token or file parameter.";
|
||||
exit;
|
||||
}
|
||||
|
||||
// Load the share folder records.
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
http_response_code(404);
|
||||
echo "Share link not found.";
|
||||
exit;
|
||||
}
|
||||
|
||||
$shareLinks = json_decode(file_get_contents($shareFile), true);
|
||||
if (!is_array($shareLinks) || !isset($shareLinks[$token])) {
|
||||
http_response_code(404);
|
||||
echo "Share link not found.";
|
||||
exit;
|
||||
}
|
||||
|
||||
$record = $shareLinks[$token];
|
||||
|
||||
// Check if the link has expired.
|
||||
if (time() > $record['expires']) {
|
||||
http_response_code(403);
|
||||
echo "This share link has expired.";
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get the shared folder from the record.
|
||||
$folder = trim($record['folder'], "/\\ ");
|
||||
$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
|
||||
$realFolderPath = realpath($folderPath);
|
||||
$uploadDirReal = realpath(UPLOAD_DIR);
|
||||
|
||||
if ($realFolderPath === false || strpos($realFolderPath, $uploadDirReal) !== 0 || !is_dir($realFolderPath)) {
|
||||
http_response_code(404);
|
||||
echo "Shared folder not found.";
|
||||
exit;
|
||||
}
|
||||
|
||||
// Sanitize the filename to prevent directory traversal.
|
||||
if (strpos($file, "/") !== false || strpos($file, "\\") !== false) {
|
||||
http_response_code(400);
|
||||
echo "Invalid file name.";
|
||||
exit;
|
||||
}
|
||||
$file = basename($file);
|
||||
|
||||
// Build the full file path and verify it is inside the shared folder.
|
||||
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
|
||||
$realFilePath = realpath($filePath);
|
||||
if ($realFilePath === false || strpos($realFilePath, $realFolderPath) !== 0 || !is_file($realFilePath)) {
|
||||
http_response_code(404);
|
||||
echo "File not found.";
|
||||
exit;
|
||||
}
|
||||
|
||||
// Determine MIME type.
|
||||
$mimeType = mime_content_type($realFilePath);
|
||||
header("Content-Type: " . $mimeType);
|
||||
|
||||
// Set Content-Disposition header.
|
||||
// Inline if the file is an image; attachment for others.
|
||||
$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
|
||||
header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"');
|
||||
} else {
|
||||
header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
|
||||
}
|
||||
|
||||
// Disable caching.
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
|
||||
// Read and output the file.
|
||||
readfile($realFilePath);
|
||||
exit;
|
||||
?>
|
||||
133
downloadZip.php
133
downloadZip.php
@@ -1,133 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if the user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Read and decode the JSON input.
|
||||
$rawData = file_get_contents("php://input");
|
||||
$data = json_decode($rawData, true);
|
||||
|
||||
if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = $data['folder'];
|
||||
$files = $data['files'];
|
||||
|
||||
// Validate folder name to allow subfolders.
|
||||
// "root" is allowed; otherwise, split by "/" and validate each segment.
|
||||
if ($folder !== "root") {
|
||||
$parts = explode('/', $folder);
|
||||
foreach ($parts as $part) {
|
||||
if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $part)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR;
|
||||
} else {
|
||||
$relativePath = "";
|
||||
}
|
||||
|
||||
// Use the absolute UPLOAD_DIR from config.php.
|
||||
$baseDir = realpath(UPLOAD_DIR);
|
||||
if ($baseDir === false) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Uploads directory not configured correctly."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath;
|
||||
$folderPathReal = realpath($folderPath);
|
||||
if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Folder not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($files)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "No files specified."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
foreach ($files as $fileName) {
|
||||
if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $fileName)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Invalid file name: " . $fileName]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Build an array of files to include in the ZIP.
|
||||
$filesToZip = [];
|
||||
foreach ($files as $fileName) {
|
||||
$filePath = $folderPathReal . DIRECTORY_SEPARATOR . $fileName;
|
||||
if (file_exists($filePath)) {
|
||||
$filesToZip[] = $filePath;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($filesToZip)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "No valid files found to zip."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Create a temporary file for the ZIP archive.
|
||||
$tempZip = tempnam(sys_get_temp_dir(), 'zip');
|
||||
unlink($tempZip); // Remove the temporary file so ZipArchive can create a new one.
|
||||
$tempZip .= '.zip';
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($tempZip, ZipArchive::CREATE) !== TRUE) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Could not create zip archive."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Add each file to the archive using its base name.
|
||||
foreach ($filesToZip as $filePath) {
|
||||
$zip->addFile($filePath, basename($filePath));
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
// Send headers to force download and disable caching.
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Disposition: attachment; filename="files.zip"');
|
||||
header('Content-Length: ' . filesize($tempZip));
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
// Output the file and delete it afterward.
|
||||
readfile($tempZip);
|
||||
unlink($tempZip);
|
||||
exit;
|
||||
?>
|
||||
165
extractZip.php
165
extractZip.php
@@ -1,165 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to extract zip files"]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Read and decode the JSON input.
|
||||
$rawData = file_get_contents("php://input");
|
||||
$data = json_decode($rawData, true);
|
||||
if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = $data['folder'];
|
||||
$files = $data['files'];
|
||||
|
||||
if (empty($files)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "No files specified."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate folder name (allow "root" or valid subfolder names).
|
||||
if ($folder !== "root") {
|
||||
$parts = explode('/', $folder);
|
||||
foreach ($parts as $part) {
|
||||
if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $part)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR;
|
||||
} else {
|
||||
$relativePath = "";
|
||||
}
|
||||
|
||||
$baseDir = realpath(UPLOAD_DIR);
|
||||
if ($baseDir === false) {
|
||||
http_response_code(500);
|
||||
echo json_encode(["error" => "Uploads directory not configured correctly."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath;
|
||||
$folderPathReal = realpath($folderPath);
|
||||
if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) {
|
||||
http_response_code(404);
|
||||
echo json_encode(["error" => "Folder not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ---------- Metadata Setup ----------
|
||||
function getMetadataFilePath($folder) {
|
||||
if (strtolower($folder) === 'root' || $folder === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
|
||||
}
|
||||
|
||||
$srcMetaFile = getMetadataFilePath($folder);
|
||||
$destMetaFile = getMetadataFilePath($folder);
|
||||
$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : [];
|
||||
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
|
||||
|
||||
$errors = [];
|
||||
$allSuccess = true;
|
||||
$extractedFiles = array(); // Array to collect names of extracted files
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
|
||||
// ---------- Process Each File ----------
|
||||
foreach ($files as $zipFileName) {
|
||||
$originalName = basename(trim($zipFileName));
|
||||
// Process only .zip files.
|
||||
if (strtolower(substr($originalName, -4)) !== '.zip') {
|
||||
continue;
|
||||
}
|
||||
if (!preg_match($safeFileNamePattern, $originalName)) {
|
||||
$errors[] = "$originalName has an invalid name.";
|
||||
$allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$zipFilePath = $folderPathReal . DIRECTORY_SEPARATOR . $originalName;
|
||||
if (!file_exists($zipFilePath)) {
|
||||
$errors[] = "$originalName does not exist in folder.";
|
||||
$allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipFilePath) !== TRUE) {
|
||||
$errors[] = "Could not open $originalName as a zip file.";
|
||||
$allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attempt extraction.
|
||||
if (!$zip->extractTo($folderPathReal)) {
|
||||
$errors[] = "Failed to extract $originalName.";
|
||||
$allSuccess = false;
|
||||
} else {
|
||||
// Collect extracted file names from this zip.
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$entryName = $zip->getNameIndex($i);
|
||||
$extractedFileName = basename($entryName);
|
||||
if ($extractedFileName) {
|
||||
$extractedFiles[] = $extractedFileName;
|
||||
}
|
||||
}
|
||||
// Update metadata for each extracted file if the zip file has metadata.
|
||||
if (isset($srcMetadata[$originalName])) {
|
||||
$zipMeta = $srcMetadata[$originalName];
|
||||
// Iterate through all entries in the zip.
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$entryName = $zip->getNameIndex($i);
|
||||
$extractedFileName = basename($entryName);
|
||||
if ($extractedFileName) {
|
||||
$destMetadata[$extractedFileName] = $zipMeta;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
// Write updated metadata back to the destination metadata file.
|
||||
if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) {
|
||||
$errors[] = "Failed to update metadata.";
|
||||
$allSuccess = false;
|
||||
}
|
||||
|
||||
if ($allSuccess) {
|
||||
echo json_encode(["success" => true, "extractedFiles" => $extractedFiles]);
|
||||
} else {
|
||||
echo json_encode(["success" => false, "error" => implode(" ", $errors)]);
|
||||
}
|
||||
exit;
|
||||
?>
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$configFile = USERS_DIR . 'adminConfig.json';
|
||||
if (file_exists($configFile)) {
|
||||
$encryptedContent = file_get_contents($configFile);
|
||||
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
||||
if ($decryptedContent === false) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Failed to decrypt configuration.']);
|
||||
exit;
|
||||
}
|
||||
// Decode the configuration and ensure globalOtpauthUrl is set
|
||||
$config = json_decode($decryptedContent, true);
|
||||
if (!isset($config['globalOtpauthUrl'])) {
|
||||
$config['globalOtpauthUrl'] = "";
|
||||
}
|
||||
echo json_encode($config);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'oidc' => [
|
||||
'providerUrl' => 'https://your-oidc-provider.com',
|
||||
'clientId' => 'YOUR_CLIENT_ID',
|
||||
'clientSecret' => 'YOUR_CLIENT_SECRET',
|
||||
'redirectUri' => 'https://yourdomain.com/auth.php?oidc=callback'
|
||||
],
|
||||
'loginOptions' => [
|
||||
'disableFormLogin' => false,
|
||||
'disableBasicAuth' => false,
|
||||
'disableOIDCLogin' => false
|
||||
],
|
||||
'globalOtpauthUrl' => ""
|
||||
]);
|
||||
}
|
||||
?>
|
||||
106
getFileList.php
106
getFileList.php
@@ -1,106 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
|
||||
// Allow only safe characters in the folder parameter (letters, numbers, underscores, dashes, spaces, and forward slashes).
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Determine the directory based on the folder parameter.
|
||||
if ($folder !== 'root') {
|
||||
$directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
|
||||
} else {
|
||||
$directory = UPLOAD_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Generate the metadata file path for a given folder.
|
||||
* For "root", returns "root_metadata.json". Otherwise, replaces slashes,
|
||||
* backslashes, and spaces with dashes and appends "_metadata.json".
|
||||
*
|
||||
* @param string $folder The folder's relative path.
|
||||
* @return string The full path to the folder's metadata file.
|
||||
*/
|
||||
function getMetadataFilePath($folder) {
|
||||
if (strtolower($folder) === 'root' || $folder === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
|
||||
}
|
||||
|
||||
$metadataFile = getMetadataFilePath($folder);
|
||||
$metadata = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : [];
|
||||
|
||||
if (!is_dir($directory)) {
|
||||
echo json_encode(["error" => "Directory not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$files = array_values(array_diff(scandir($directory), array('.', '..')));
|
||||
$fileList = [];
|
||||
|
||||
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
|
||||
foreach ($files as $file) {
|
||||
// Skip hidden files (those that begin with a dot)
|
||||
if (substr($file, 0, 1) === '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filePath = $directory . DIRECTORY_SEPARATOR . $file;
|
||||
// Only include files (skip directories)
|
||||
if (!is_file($filePath)) continue;
|
||||
|
||||
// Optionally, skip files with unsafe names.
|
||||
if (!preg_match($safeFileNamePattern, $file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Since metadata is stored per folder, the key is simply the file name.
|
||||
$metaKey = $file;
|
||||
|
||||
$fileDateModified = filemtime($filePath) ? date(DATE_TIME_FORMAT, filemtime($filePath)) : "Unknown";
|
||||
$fileUploadedDate = isset($metadata[$metaKey]["uploaded"]) ? $metadata[$metaKey]["uploaded"] : "Unknown";
|
||||
$fileUploader = isset($metadata[$metaKey]["uploader"]) ? $metadata[$metaKey]["uploader"] : "Unknown";
|
||||
|
||||
$fileSizeBytes = filesize($filePath);
|
||||
if ($fileSizeBytes >= 1073741824) {
|
||||
$fileSizeFormatted = sprintf("%.1f GB", $fileSizeBytes / 1073741824);
|
||||
} elseif ($fileSizeBytes >= 1048576) {
|
||||
$fileSizeFormatted = sprintf("%.1f MB", $fileSizeBytes / 1048576);
|
||||
} elseif ($fileSizeBytes >= 1024) {
|
||||
$fileSizeFormatted = sprintf("%.1f KB", $fileSizeBytes / 1024);
|
||||
} else {
|
||||
$fileSizeFormatted = sprintf("%s bytes", number_format($fileSizeBytes));
|
||||
}
|
||||
|
||||
$fileList[] = [
|
||||
'name' => $file,
|
||||
'modified' => $fileDateModified,
|
||||
'uploaded' => $fileUploadedDate,
|
||||
'size' => $fileSizeFormatted,
|
||||
'uploader' => $fileUploader,
|
||||
'tags' => isset($metadata[$metaKey]['tags']) ? $metadata[$metaKey]['tags'] : []
|
||||
];
|
||||
}
|
||||
|
||||
// Load global tags from createdTags.json.
|
||||
$globalTagsFile = META_DIR . "createdTags.json";
|
||||
$globalTags = file_exists($globalTagsFile) ? json_decode(file_get_contents($globalTagsFile), true) : [];
|
||||
|
||||
echo json_encode(["files" => $fileList, "globalTags" => $globalTags]);
|
||||
?>
|
||||
@@ -1,97 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scan a directory for subfolders.
|
||||
*
|
||||
* @param string $dir The full path to the directory.
|
||||
* @param string $relative The relative path from the base upload directory.
|
||||
* @return array An array of folder paths (relative to the base).
|
||||
*/
|
||||
function getSubfolders($dir, $relative = '') {
|
||||
$folders = [];
|
||||
$items = scandir($dir);
|
||||
// Allow letters, numbers, underscores, dashes, and spaces in folder names.
|
||||
$safeFolderNamePattern = '/^[A-Za-z0-9_\- ]+$/';
|
||||
foreach ($items as $item) {
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
if (!preg_match($safeFolderNamePattern, $item)) {
|
||||
continue;
|
||||
}
|
||||
$path = $dir . DIRECTORY_SEPARATOR . $item;
|
||||
if (is_dir($path)) {
|
||||
// Build the relative path.
|
||||
$folderPath = ($relative ? $relative . '/' : '') . $item;
|
||||
$folders[] = $folderPath;
|
||||
// Recursively get subfolders.
|
||||
$subFolders = getSubfolders($path, $folderPath);
|
||||
$folders = array_merge($folders, $subFolders);
|
||||
}
|
||||
}
|
||||
return $folders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Generate the metadata file path for a given folder.
|
||||
* For "root", it returns "root_metadata.json"; otherwise, it replaces
|
||||
* slashes, backslashes, and spaces with dashes and appends "_metadata.json".
|
||||
*
|
||||
* @param string $folder The folder's relative path.
|
||||
* @return string The full path to the folder's metadata file.
|
||||
*/
|
||||
function getMetadataFilePath($folder) {
|
||||
if (strtolower($folder) === 'root' || $folder === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
|
||||
}
|
||||
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
|
||||
// Build an array to hold folder information.
|
||||
$folderInfoList = [];
|
||||
|
||||
// Include "root" as a folder.
|
||||
$rootMetaFile = getMetadataFilePath('root');
|
||||
$rootFileCount = 0;
|
||||
if (file_exists($rootMetaFile)) {
|
||||
$rootMetadata = json_decode(file_get_contents($rootMetaFile), true);
|
||||
$rootFileCount = is_array($rootMetadata) ? count($rootMetadata) : 0;
|
||||
}
|
||||
$folderInfoList[] = [
|
||||
"folder" => "root",
|
||||
"fileCount" => $rootFileCount,
|
||||
"metadataFile" => basename($rootMetaFile)
|
||||
];
|
||||
|
||||
// Scan for subfolders.
|
||||
$subfolders = [];
|
||||
if (is_dir($baseDir)) {
|
||||
$subfolders = getSubfolders($baseDir);
|
||||
}
|
||||
|
||||
// For each subfolder, load its metadata and record file count.
|
||||
foreach ($subfolders as $folder) {
|
||||
$metaFile = getMetadataFilePath($folder);
|
||||
$fileCount = 0;
|
||||
if (file_exists($metaFile)) {
|
||||
$metadata = json_decode(file_get_contents($metaFile), true);
|
||||
$fileCount = is_array($metadata) ? count($metadata) : 0;
|
||||
}
|
||||
$folderInfoList[] = [
|
||||
"folder" => $folder,
|
||||
"fileCount" => $fileCount,
|
||||
"metadataFile" => basename($metaFile)
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode($folderInfoList);
|
||||
?>
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Define the trash directory and trash metadata file.
|
||||
$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||
$trashMetadataFile = $trashDir . "trash.json";
|
||||
|
||||
// Helper: Generate the metadata file path for a given folder.
|
||||
// For "root", returns "root_metadata.json". Otherwise, replaces slashes, backslashes, and spaces with dashes and appends "_metadata.json".
|
||||
function getMetadataFilePath($folder) {
|
||||
if (strtolower($folder) === 'root' || $folder === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
|
||||
}
|
||||
|
||||
// Read the trash metadata.
|
||||
$trashItems = [];
|
||||
if (file_exists($trashMetadataFile)) {
|
||||
$json = file_get_contents($trashMetadataFile);
|
||||
$trashItems = json_decode($json, true);
|
||||
if (!is_array($trashItems)) {
|
||||
$trashItems = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich each trash record.
|
||||
foreach ($trashItems as &$item) {
|
||||
// Ensure deletedBy is set and not empty.
|
||||
if (empty($item['deletedBy'])) {
|
||||
$item['deletedBy'] = "Unknown";
|
||||
}
|
||||
// Enrich with uploader and uploaded date if not already present.
|
||||
if (empty($item['uploaded']) || empty($item['uploader'])) {
|
||||
if (isset($item['originalFolder']) && isset($item['originalName'])) {
|
||||
$metadataFile = getMetadataFilePath($item['originalFolder']);
|
||||
if (file_exists($metadataFile)) {
|
||||
$metadata = json_decode(file_get_contents($metadataFile), true);
|
||||
if (is_array($metadata) && isset($metadata[$item['originalName']])) {
|
||||
$item['uploaded'] = !empty($metadata[$item['originalName']]['uploaded']) ? $metadata[$item['originalName']]['uploaded'] : "Unknown";
|
||||
$item['uploader'] = !empty($metadata[$item['originalName']]['uploader']) ? $metadata[$item['originalName']]['uploader'] : "Unknown";
|
||||
} else {
|
||||
$item['uploaded'] = "Unknown";
|
||||
$item['uploader'] = "Unknown";
|
||||
}
|
||||
} else {
|
||||
$item['uploaded'] = "Unknown";
|
||||
$item['uploader'] = "Unknown";
|
||||
}
|
||||
} else {
|
||||
$item['uploaded'] = "Unknown";
|
||||
$item['uploader'] = "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
|
||||
echo json_encode($trashItems);
|
||||
exit;
|
||||
?>
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Check if the user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$permissionsFile = USERS_DIR . "userPermissions.json";
|
||||
$permissionsArray = [];
|
||||
|
||||
// Load permissions file if it exists.
|
||||
if (file_exists($permissionsFile)) {
|
||||
$content = file_get_contents($permissionsFile);
|
||||
// Attempt to decrypt the content.
|
||||
$decryptedContent = decryptData($content, $encryptionKey);
|
||||
if ($decryptedContent === false) {
|
||||
// If decryption fails, assume the file is plain JSON.
|
||||
$permissionsArray = json_decode($content, true);
|
||||
} else {
|
||||
$permissionsArray = json_decode($decryptedContent, true);
|
||||
}
|
||||
if (!is_array($permissionsArray)) {
|
||||
$permissionsArray = [];
|
||||
}
|
||||
}
|
||||
|
||||
// If the user is an admin, return all permissions.
|
||||
if (isset($_SESSION['isAdmin']) && $_SESSION['isAdmin'] === true) {
|
||||
echo json_encode($permissionsArray);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Otherwise, return only the current user's permissions.
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
foreach ($permissionsArray as $storedUsername => $data) {
|
||||
if (strcasecmp($storedUsername, $username) === 0) {
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// If no permissions are found for the current user, return an empty object.
|
||||
echo json_encode(new stdClass());
|
||||
?>
|
||||
31
getUsers.php
31
getUsers.php
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
||||
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
$users = [];
|
||||
|
||||
if (file_exists($usersFile)) {
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(':', trim($line));
|
||||
if (count($parts) >= 3) {
|
||||
// Validate username format:
|
||||
if (preg_match('/^[A-Za-z0-9_\- ]+$/', $parts[0])) {
|
||||
$users[] = [
|
||||
"username" => $parts[0],
|
||||
"role" => trim($parts[2])
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($users);
|
||||
?>
|
||||
456
js/auth.js
456
js/auth.js
@@ -1,456 +0,0 @@
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
import {
|
||||
toggleVisibility,
|
||||
showToast as originalShowToast,
|
||||
attachEnterKeyListener,
|
||||
showCustomConfirmModal
|
||||
} from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { initFileActions } from './fileActions.js';
|
||||
import { renderFileTable } from './fileListView.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import {
|
||||
openTOTPLoginModal as originalOpenTOTPLoginModal,
|
||||
openUserPanel,
|
||||
openTOTPModal,
|
||||
closeTOTPModal,
|
||||
openAdminPanel,
|
||||
closeAdminPanel,
|
||||
setLastLoginData
|
||||
} from './authModals.js';
|
||||
|
||||
// Production OIDC configuration (override via API as needed)
|
||||
const currentOIDCConfig = {
|
||||
providerUrl: "https://your-oidc-provider.com",
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET",
|
||||
redirectUri: "https://yourdomain.com/auth.php?oidc=callback",
|
||||
globalOtpauthUrl: ""
|
||||
};
|
||||
window.currentOIDCConfig = currentOIDCConfig;
|
||||
|
||||
/* ----------------- TOTP & Toast Overrides ----------------- */
|
||||
// detect if we’re in a pending‑TOTP state
|
||||
window.pendingTOTP = new URLSearchParams(window.location.search).get('totp_required') === '1';
|
||||
|
||||
// override showToast to suppress the "Please log in to continue." toast during TOTP
|
||||
function showToast(msgKey) {
|
||||
const msg = t(msgKey);
|
||||
if (window.pendingTOTP && msgKey === "please_log_in_to_continue") {
|
||||
return;
|
||||
}
|
||||
originalShowToast(msg);
|
||||
}
|
||||
window.showToast = showToast;
|
||||
|
||||
// wrap the TOTP modal opener to disable other login buttons only for Basic/OIDC flows
|
||||
function openTOTPLoginModal() {
|
||||
originalOpenTOTPLoginModal();
|
||||
|
||||
const isFormLogin = Boolean(window.__lastLoginData);
|
||||
if (!isFormLogin) {
|
||||
// disable Basic‑Auth link
|
||||
const basicLink = document.querySelector("a[href='login_basic.php']");
|
||||
if (basicLink) {
|
||||
basicLink.style.pointerEvents = 'none';
|
||||
basicLink.style.opacity = '0.5';
|
||||
}
|
||||
// disable OIDC button
|
||||
const oidcBtn = document.getElementById("oidcLoginBtn");
|
||||
if (oidcBtn) {
|
||||
oidcBtn.disabled = true;
|
||||
oidcBtn.style.opacity = '0.5';
|
||||
}
|
||||
// hide the form login
|
||||
const authForm = document.getElementById("authForm");
|
||||
if (authForm) authForm.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------- Utility Functions ----------------- */
|
||||
function updateItemsPerPageSelect() {
|
||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||
if (selectElem) {
|
||||
selectElem.value = localStorage.getItem("itemsPerPage") || "10";
|
||||
}
|
||||
}
|
||||
|
||||
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
|
||||
const authForm = document.getElementById("authForm");
|
||||
if (authForm) authForm.style.display = disableFormLogin ? "none" : "block";
|
||||
const basicAuthLink = document.querySelector("a[href='login_basic.php']");
|
||||
if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
|
||||
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||
if (oidcLoginBtn) oidcLoginBtn.style.display = disableOIDCLogin ? "none" : "inline-block";
|
||||
}
|
||||
|
||||
function updateLoginOptionsUIFromStorage() {
|
||||
updateLoginOptionsUI({
|
||||
disableFormLogin: localStorage.getItem("disableFormLogin") === "true",
|
||||
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
|
||||
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true"
|
||||
});
|
||||
}
|
||||
|
||||
function loadAdminConfigFunc() {
|
||||
return fetch("getConfig.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
localStorage.setItem("disableFormLogin", config.loginOptions.disableFormLogin);
|
||||
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
|
||||
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
|
||||
localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
|
||||
updateLoginOptionsUIFromStorage();
|
||||
})
|
||||
.catch(() => {
|
||||
localStorage.setItem("disableFormLogin", "false");
|
||||
localStorage.setItem("disableBasicAuth", "false");
|
||||
localStorage.setItem("disableOIDCLogin", "false");
|
||||
localStorage.setItem("globalOtpauthUrl", "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
|
||||
updateLoginOptionsUIFromStorage();
|
||||
});
|
||||
}
|
||||
|
||||
function insertAfter(newNode, referenceNode) {
|
||||
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
|
||||
}
|
||||
|
||||
function updateAuthenticatedUI(data) {
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", true);
|
||||
toggleVisibility("uploadFileForm", true);
|
||||
toggleVisibility("fileListContainer", true);
|
||||
attachEnterKeyListener("addUserModal", "saveUserBtn");
|
||||
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
|
||||
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||
|
||||
if (typeof data.totp_enabled !== "undefined") {
|
||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||
}
|
||||
if (data.username) {
|
||||
localStorage.setItem("username", data.username);
|
||||
}
|
||||
if (typeof data.folderOnly !== "undefined") {
|
||||
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
|
||||
}
|
||||
|
||||
const headerButtons = document.querySelector(".header-buttons");
|
||||
const firstButton = headerButtons.firstElementChild;
|
||||
|
||||
if (data.isAdmin) {
|
||||
let restoreBtn = document.getElementById("restoreFilesBtn");
|
||||
if (!restoreBtn) {
|
||||
restoreBtn = document.createElement("button");
|
||||
restoreBtn.id = "restoreFilesBtn";
|
||||
restoreBtn.classList.add("btn", "btn-warning");
|
||||
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
||||
if (firstButton) insertAfter(restoreBtn, firstButton);
|
||||
else headerButtons.appendChild(restoreBtn);
|
||||
}
|
||||
restoreBtn.style.display = "block";
|
||||
|
||||
let adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||
if (!adminPanelBtn) {
|
||||
adminPanelBtn = document.createElement("button");
|
||||
adminPanelBtn.id = "adminPanelBtn";
|
||||
adminPanelBtn.classList.add("btn", "btn-info");
|
||||
adminPanelBtn.innerHTML = '<i class="material-icons" title="Admin Panel">admin_panel_settings</i>';
|
||||
insertAfter(adminPanelBtn, restoreBtn);
|
||||
adminPanelBtn.addEventListener("click", openAdminPanel);
|
||||
} else {
|
||||
adminPanelBtn.style.display = "block";
|
||||
}
|
||||
} else {
|
||||
const restoreBtn = document.getElementById("restoreFilesBtn");
|
||||
if (restoreBtn) restoreBtn.style.display = "none";
|
||||
const adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||
if (adminPanelBtn) adminPanelBtn.style.display = "none";
|
||||
}
|
||||
|
||||
if (window.location.hostname !== "demo.filerise.net") {
|
||||
let userPanelBtn = document.getElementById("userPanelBtn");
|
||||
if (!userPanelBtn) {
|
||||
userPanelBtn = document.createElement("button");
|
||||
userPanelBtn.id = "userPanelBtn";
|
||||
userPanelBtn.classList.add("btn", "btn-user");
|
||||
userPanelBtn.innerHTML = '<i class="material-icons" title="User Panel">account_circle</i>';
|
||||
const adminBtn = document.getElementById("adminPanelBtn");
|
||||
if (adminBtn) insertAfter(userPanelBtn, adminBtn);
|
||||
else if (firstButton) insertAfter(userPanelBtn, firstButton);
|
||||
else headerButtons.appendChild(userPanelBtn);
|
||||
userPanelBtn.addEventListener("click", openUserPanel);
|
||||
} else {
|
||||
userPanelBtn.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
updateItemsPerPageSelect();
|
||||
updateLoginOptionsUIFromStorage();
|
||||
}
|
||||
|
||||
function checkAuthentication(showLoginToast = true) {
|
||||
return sendRequest("checkAuth.php")
|
||||
.then(data => {
|
||||
if (data.setup) {
|
||||
window.setupMode = true;
|
||||
if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user.");
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById("newUsername").focus();
|
||||
return false;
|
||||
}
|
||||
window.setupMode = false;
|
||||
if (data.authenticated) {
|
||||
if (typeof data.totp_enabled !== "undefined") {
|
||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||
}
|
||||
updateAuthenticatedUI(data);
|
||||
return data;
|
||||
} else {
|
||||
if (showLoginToast) showToast("Please log in to continue.");
|
||||
toggleVisibility("loginForm", true);
|
||||
toggleVisibility("mainOperations", false);
|
||||
toggleVisibility("uploadFileForm", false);
|
||||
toggleVisibility("fileListContainer", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
/* ----------------- Authentication Submission ----------------- */
|
||||
function submitLogin(data) {
|
||||
setLastLoginData(data);
|
||||
window.__lastLoginData = data;
|
||||
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success || response.status === "ok") {
|
||||
sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!");
|
||||
window.location.reload();
|
||||
} else if (response.totp_required) {
|
||||
openTOTPLoginModal();
|
||||
} else if (response.error && response.error.includes("Too many failed login attempts")) {
|
||||
showToast(response.error);
|
||||
const loginButton = document.getElementById("authForm").querySelector("button[type='submit']");
|
||||
if (loginButton) {
|
||||
loginButton.disabled = true;
|
||||
setTimeout(() => {
|
||||
loginButton.disabled = false;
|
||||
showToast("You can now try logging in again.");
|
||||
}, 30 * 60 * 1000);
|
||||
}
|
||||
} else {
|
||||
showToast("Login failed: " + (response.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast("Login failed: Unknown error");
|
||||
});
|
||||
}
|
||||
window.submitLogin = submitLogin;
|
||||
|
||||
/* ----------------- Other Helpers ----------------- */
|
||||
window.changeItemsPerPage = function (value) {
|
||||
localStorage.setItem("itemsPerPage", value);
|
||||
if (typeof renderFileTable === "function") renderFileTable(window.currentFolder || "root");
|
||||
};
|
||||
|
||||
function resetUserForm() {
|
||||
document.getElementById("newUsername").value = "";
|
||||
document.getElementById("addUserPassword").value = "";
|
||||
}
|
||||
|
||||
function closeAddUserModal() {
|
||||
toggleVisibility("addUserModal", false);
|
||||
resetUserForm();
|
||||
}
|
||||
|
||||
function closeRemoveUserModal() {
|
||||
toggleVisibility("removeUserModal", false);
|
||||
document.getElementById("removeUsernameSelect").innerHTML = "";
|
||||
}
|
||||
|
||||
function loadUserList() {
|
||||
fetch("getUsers.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const users = Array.isArray(data) ? data : (data.users || []);
|
||||
const selectElem = document.getElementById("removeUsernameSelect");
|
||||
selectElem.innerHTML = "";
|
||||
users.forEach(user => {
|
||||
const option = document.createElement("option");
|
||||
option.value = user.username;
|
||||
option.textContent = user.username;
|
||||
selectElem.appendChild(option);
|
||||
});
|
||||
if (selectElem.options.length === 0) {
|
||||
showToast("No other users found to remove.");
|
||||
closeRemoveUserModal();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
window.loadUserList = loadUserList;
|
||||
|
||||
function initAuth() {
|
||||
checkAuthentication(false);
|
||||
loadAdminConfigFunc();
|
||||
const authForm = document.getElementById("authForm");
|
||||
if (authForm) {
|
||||
authForm.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
const rememberMe = document.getElementById("rememberMeCheckbox")
|
||||
? document.getElementById("rememberMeCheckbox").checked
|
||||
: false;
|
||||
const formData = {
|
||||
username: document.getElementById("loginUsername").value.trim(),
|
||||
password: document.getElementById("loginPassword").value.trim(),
|
||||
remember_me: rememberMe
|
||||
};
|
||||
submitLogin(formData);
|
||||
});
|
||||
}
|
||||
document.getElementById("logoutBtn").addEventListener("click", function () {
|
||||
fetch("logout.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
}).then(() => window.location.reload(true)).catch(() => {});
|
||||
});
|
||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||
resetUserForm();
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById("newUsername").focus();
|
||||
});
|
||||
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
||||
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 = "addUser.php";
|
||||
if (window.setupMode) url += "?setup=1";
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||
})
|
||||
.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(() => { });
|
||||
});
|
||||
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
|
||||
|
||||
document.getElementById("removeUserBtn").addEventListener("click", function () {
|
||||
loadUserList();
|
||||
toggleVisibility("removeUserModal", true);
|
||||
});
|
||||
document.getElementById("deleteUserBtn").addEventListener("click", async function () {
|
||||
const selectElem = document.getElementById("removeUsernameSelect");
|
||||
const usernameToRemove = selectElem.value;
|
||||
if (!usernameToRemove) {
|
||||
showToast("Please select a user to remove.");
|
||||
return;
|
||||
}
|
||||
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
||||
if (!confirmed) return;
|
||||
fetch("removeUser.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
body: JSON.stringify({ username: usernameToRemove })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("User removed successfully!");
|
||||
closeRemoveUserModal();
|
||||
loadUserList();
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not remove user"));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
|
||||
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
||||
document.getElementById("changePasswordModal").style.display = "block";
|
||||
document.getElementById("oldPassword").focus();
|
||||
});
|
||||
document.getElementById("closeChangePasswordModal").addEventListener("click", function () {
|
||||
document.getElementById("changePasswordModal").style.display = "none";
|
||||
});
|
||||
document.getElementById("saveNewPasswordBtn").addEventListener("click", function () {
|
||||
const oldPassword = document.getElementById("oldPassword").value.trim();
|
||||
const newPassword = document.getElementById("newPassword").value.trim();
|
||||
const confirmPassword = document.getElementById("confirmPassword").value.trim();
|
||||
if (!oldPassword || !newPassword || !confirmPassword) {
|
||||
showToast("Please fill in all fields.");
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
showToast("New passwords do not match.");
|
||||
return;
|
||||
}
|
||||
const data = { oldPassword, newPassword, confirmPassword };
|
||||
fetch("changePassword.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
showToast(result.success);
|
||||
document.getElementById("oldPassword").value = "";
|
||||
document.getElementById("newPassword").value = "";
|
||||
document.getElementById("confirmPassword").value = "";
|
||||
document.getElementById("changePasswordModal").style.display = "none";
|
||||
} else {
|
||||
showToast("Error: " + (result.error || "Could not change password."));
|
||||
}
|
||||
})
|
||||
.catch(() => { showToast("Error changing password."); });
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
updateItemsPerPageSelect();
|
||||
updateLoginOptionsUI({
|
||||
disableFormLogin: localStorage.getItem("disableFormLogin") === "true",
|
||||
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
|
||||
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true"
|
||||
});
|
||||
|
||||
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||
if (oidcLoginBtn) {
|
||||
oidcLoginBtn.addEventListener("click", () => {
|
||||
window.location.href = "auth.php?oidc=initiate";
|
||||
});
|
||||
}
|
||||
|
||||
// If TOTP is pending, show modal and skip normal auth init
|
||||
if (window.pendingTOTP) {
|
||||
openTOTPLoginModal();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
export { initAuth, checkAuthentication };
|
||||
840
js/authModals.js
840
js/authModals.js
@@ -1,840 +0,0 @@
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
|
||||
const version = "v1.1.1";
|
||||
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||
|
||||
let lastLoginData = null;
|
||||
export function setLastLoginData(data) {
|
||||
lastLoginData = data;
|
||||
// expose to auth.js so it can tell form-login vs basic/oidc
|
||||
//window.__lastLoginData = data;
|
||||
}
|
||||
|
||||
export function openTOTPLoginModal() {
|
||||
let totpLoginModal = document.getElementById("totpLoginModal");
|
||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||
const modalBg = isDarkMode ? "#2c2c2c" : "#fff";
|
||||
const textColor = isDarkMode ? "#e0e0e0" : "#000";
|
||||
|
||||
if (!totpLoginModal) {
|
||||
totpLoginModal = document.createElement("div");
|
||||
totpLoginModal.id = "totpLoginModal";
|
||||
totpLoginModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100vw; height: 100vh;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
z-index: 3200;
|
||||
`;
|
||||
totpLoginModal.innerHTML = `
|
||||
<div style="background: ${modalBg}; padding:20px; border-radius:8px; text-align:center; position:relative; color:${textColor};">
|
||||
<span id="closeTOTPLoginModal" style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">×</span>
|
||||
<div id="totpSection">
|
||||
<h3>${t("enter_totp_code")}</h3>
|
||||
<input type="text" id="totpLoginInput" maxlength="6"
|
||||
style="font-size:24px; text-align:center; width:100%; padding:10px;"
|
||||
placeholder="6-digit code" />
|
||||
</div>
|
||||
<a href="#" id="toggleRecovery" style="display:block; margin-top:10px; font-size:14px;">${t("use_recovery_code_instead")}</a>
|
||||
<div id="recoverySection" style="display:none; margin-top:10px;">
|
||||
<h3>${t("enter_recovery_code")}</h3>
|
||||
<input type="text" id="recoveryInput"
|
||||
style="font-size:24px; text-align:center; width:100%; padding:10px;"
|
||||
placeholder="Recovery code" />
|
||||
<button type="button" id="submitRecovery" class="btn btn-secondary" style="margin-top:10px;">Submit Recovery Code</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(totpLoginModal);
|
||||
|
||||
// Close button
|
||||
document.getElementById("closeTOTPLoginModal").addEventListener("click", () => {
|
||||
totpLoginModal.style.display = "none";
|
||||
});
|
||||
|
||||
// Toggle between TOTP and Recovery
|
||||
document.getElementById("toggleRecovery").addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
const totpSection = document.getElementById("totpSection");
|
||||
const recoverySection = document.getElementById("recoverySection");
|
||||
const toggleLink = this;
|
||||
|
||||
if (recoverySection.style.display === "none") {
|
||||
// Switch to recovery
|
||||
totpSection.style.display = "none";
|
||||
recoverySection.style.display = "block";
|
||||
toggleLink.textContent = "Use TOTP Code instead";
|
||||
} else {
|
||||
// Switch back to TOTP
|
||||
recoverySection.style.display = "none";
|
||||
totpSection.style.display = "block";
|
||||
toggleLink.textContent = "Use Recovery Code instead";
|
||||
}
|
||||
});
|
||||
|
||||
// Recovery submission
|
||||
document.getElementById("submitRecovery").addEventListener("click", () => {
|
||||
const recoveryCode = document.getElementById("recoveryInput").value.trim();
|
||||
if (!recoveryCode) {
|
||||
showToast("Please enter your recovery code.");
|
||||
return;
|
||||
}
|
||||
fetch("totp_recover.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ recovery_code: recoveryCode })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
if (json.status === "ok") {
|
||||
// recovery succeeded → finalize login
|
||||
window.location.href = "index.html";
|
||||
} else {
|
||||
showToast(json.message || "Recovery code verification failed");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast("Error verifying recovery code.");
|
||||
});
|
||||
});
|
||||
|
||||
// TOTP submission
|
||||
const totpInput = document.getElementById("totpLoginInput");
|
||||
totpInput.focus();
|
||||
totpInput.addEventListener("input", function () {
|
||||
const code = this.value.trim();
|
||||
if (code.length === 6) {
|
||||
fetch("totp_verify.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ totp_code: code })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
if (json.status === "ok") {
|
||||
window.location.href = "index.html";
|
||||
} else {
|
||||
showToast(json.message || "TOTP verification failed");
|
||||
this.value = "";
|
||||
totpLoginModal.style.display = "flex";
|
||||
totpInput.focus();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast("TOTP verification failed");
|
||||
this.value = "";
|
||||
totpLoginModal.style.display = "flex";
|
||||
totpInput.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Re-open existing modal
|
||||
totpLoginModal.style.display = "flex";
|
||||
const totpInput = document.getElementById("totpLoginInput");
|
||||
totpInput.value = "";
|
||||
totpInput.style.display = "block";
|
||||
totpInput.focus();
|
||||
document.getElementById("recoverySection").style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
export function openUserPanel() {
|
||||
const username = localStorage.getItem("username") || "User";
|
||||
let userPanelModal = document.getElementById("userPanelModal");
|
||||
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"};
|
||||
transform: none;
|
||||
transition: none;
|
||||
`;
|
||||
// Retrieve the language setting from local storage, default to English ("en")
|
||||
const savedLanguage = localStorage.getItem("language") || "en";
|
||||
if (!userPanelModal) {
|
||||
userPanelModal = document.createElement("div");
|
||||
userPanelModal.id = "userPanelModal";
|
||||
userPanelModal.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;
|
||||
`;
|
||||
userPanelModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeUserPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>User Panel (${username})</h3>
|
||||
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">Change Password</button>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>TOTP Settings</legend>
|
||||
<div class="form-group">
|
||||
<label for="userTOTPEnabled">Enable TOTP:</label>
|
||||
<input type="checkbox" id="userTOTPEnabled" style="vertical-align: middle;" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>Language</legend>
|
||||
<div class="form-group">
|
||||
<label for="languageSelector">Select Language:</label>
|
||||
<select id="languageSelector">
|
||||
<option value="en">English</option>
|
||||
<option value="es">Español</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="de">Deutsch</option>
|
||||
</select>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(userPanelModal);
|
||||
// Close button handler
|
||||
document.getElementById("closeUserPanel").addEventListener("click", () => {
|
||||
userPanelModal.style.display = "none";
|
||||
});
|
||||
// Change Password button
|
||||
document.getElementById("openChangePasswordModalBtn").addEventListener("click", () => {
|
||||
document.getElementById("changePasswordModal").style.display = "block";
|
||||
});
|
||||
// TOTP checkbox behavior
|
||||
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
||||
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
|
||||
totpCheckbox.addEventListener("change", function () {
|
||||
localStorage.setItem("userTOTPEnabled", this.checked ? "true" : "false");
|
||||
const enabled = this.checked;
|
||||
fetch("updateUserPanel.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ totp_enabled: enabled })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (!result.success) {
|
||||
showToast("Error updating TOTP setting: " + result.error);
|
||||
} else if (enabled) {
|
||||
openTOTPModal();
|
||||
}
|
||||
})
|
||||
.catch(() => { showToast("Error updating TOTP setting."); });
|
||||
});
|
||||
// Language dropdown initialization
|
||||
const languageSelector = document.getElementById("languageSelector");
|
||||
languageSelector.value = savedLanguage;
|
||||
languageSelector.addEventListener("change", function () {
|
||||
const selectedLanguage = this.value;
|
||||
localStorage.setItem("language", selectedLanguage);
|
||||
setLocale(selectedLanguage);
|
||||
applyTranslations();
|
||||
});
|
||||
} else {
|
||||
// If the modal already exists, update its colors
|
||||
userPanelModal.style.backgroundColor = overlayBackground;
|
||||
const modalContent = userPanelModal.querySelector(".modal-content");
|
||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
||||
modalContent.style.border = isDarkMode ? "1px solid #444" : "1px solid #ccc";
|
||||
}
|
||||
userPanelModal.style.display = "flex";
|
||||
}
|
||||
|
||||
function showRecoveryCodeModal(recoveryCode) {
|
||||
const recoveryModal = document.createElement("div");
|
||||
recoveryModal.id = "recoveryModal";
|
||||
recoveryModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3200;
|
||||
`;
|
||||
recoveryModal.innerHTML = `
|
||||
<div style="background: #fff; color: #000; padding: 20px; max-width: 400px; width: 90%; border-radius: 8px; text-align: center;">
|
||||
<h3>Your Recovery Code</h3>
|
||||
<p>Please save this code securely. It will not be shown again and can only be used once.</p>
|
||||
<code style="display: block; margin: 10px 0; font-size: 20px;">${recoveryCode}</code>
|
||||
<button type="button" id="closeRecoveryModal" class="btn btn-primary">OK</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(recoveryModal);
|
||||
|
||||
document.getElementById("closeRecoveryModal").addEventListener("click", () => {
|
||||
recoveryModal.remove();
|
||||
});
|
||||
}
|
||||
|
||||
export function openTOTPModal() {
|
||||
let totpModal = document.getElementById("totpModal");
|
||||
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: 400px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
`;
|
||||
if (!totpModal) {
|
||||
totpModal = document.createElement("div");
|
||||
totpModal.id = "totpModal";
|
||||
totpModal.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: 3100;
|
||||
`;
|
||||
totpModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>TOTP Setup</h3>
|
||||
<p>Scan this QR code with your authenticator app:</p>
|
||||
<img src="totp_setup.php?csrf=${encodeURIComponent(window.csrfToken)}" alt="TOTP QR Code" style="max-width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||
<br/>
|
||||
<p>Enter the 6-digit code from your app to confirm setup:</p>
|
||||
<input type="text" id="totpConfirmInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
|
||||
<br/><br/>
|
||||
<button type="button" id="confirmTOTPBtn" class="btn btn-primary">Confirm</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(totpModal);
|
||||
|
||||
document.getElementById("closeTOTPModal").addEventListener("click", () => {
|
||||
closeTOTPModal(true);
|
||||
});
|
||||
|
||||
document.getElementById("confirmTOTPBtn").addEventListener("click", function () {
|
||||
const code = document.getElementById("totpConfirmInput").value.trim();
|
||||
if (code.length !== 6) {
|
||||
showToast("Please enter a valid 6-digit code.");
|
||||
return;
|
||||
}
|
||||
fetch("totp_verify.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ totp_code: code })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.status === 'ok') {
|
||||
showToast("TOTP successfully enabled.");
|
||||
// After successful TOTP verification, fetch the recovery code
|
||||
fetch("totp_saveCode.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'ok' && data.recoveryCode) {
|
||||
// Show the recovery code in a secure modal
|
||||
showRecoveryCodeModal(data.recoveryCode);
|
||||
} else {
|
||||
showToast("Error generating recovery code: " + (data.message || "Unknown error."));
|
||||
}
|
||||
})
|
||||
.catch(() => { showToast("Error generating recovery code."); });
|
||||
closeTOTPModal(false);
|
||||
} else {
|
||||
showToast("TOTP verification failed: " + (result.message || "Invalid code."));
|
||||
}
|
||||
})
|
||||
.catch(() => { showToast("Error verifying TOTP code."); });
|
||||
});
|
||||
|
||||
// Focus the input and attach enter key listener
|
||||
const totpConfirmInput = document.getElementById("totpConfirmInput");
|
||||
if (totpConfirmInput) {
|
||||
setTimeout(() => {
|
||||
const totpConfirmInput = document.getElementById("totpConfirmInput");
|
||||
if (totpConfirmInput) totpConfirmInput.focus();
|
||||
}, 100);
|
||||
}
|
||||
attachEnterKeyListener("totpModal", "confirmTOTPBtn");
|
||||
|
||||
} else {
|
||||
totpModal.style.display = "flex";
|
||||
totpModal.style.backgroundColor = overlayBackground;
|
||||
const modalContent = totpModal.querySelector(".modal-content");
|
||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
||||
|
||||
// Focus the input and attach enter key listener
|
||||
const totpConfirmInput = document.getElementById("totpConfirmInput");
|
||||
if (totpConfirmInput) {
|
||||
totpConfirmInput.value = "";
|
||||
setTimeout(() => {
|
||||
const totpConfirmInput = document.getElementById("totpConfirmInput");
|
||||
if (totpConfirmInput) totpConfirmInput.focus();
|
||||
}, 100);
|
||||
}
|
||||
attachEnterKeyListener("totpModal", "confirmTOTPBtn");
|
||||
}
|
||||
}
|
||||
|
||||
// Updated closeTOTPModal function with a disable parameter
|
||||
export function closeTOTPModal(disable = true) {
|
||||
const totpModal = document.getElementById("totpModal");
|
||||
if (totpModal) totpModal.style.display = "none";
|
||||
|
||||
if (disable) {
|
||||
// Uncheck the Enable TOTP checkbox
|
||||
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
||||
if (totpCheckbox) {
|
||||
totpCheckbox.checked = false;
|
||||
localStorage.setItem("userTOTPEnabled", "false");
|
||||
}
|
||||
// Call endpoint to remove the TOTP secret from the user's record
|
||||
fetch("totp_disable.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (!result.success) {
|
||||
showToast("Error disabling TOTP setting: " + result.error);
|
||||
}
|
||||
})
|
||||
.catch(() => { showToast("Error disabling TOTP setting."); });
|
||||
}
|
||||
}
|
||||
|
||||
export function openAdminPanel() {
|
||||
fetch("getConfig.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
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;
|
||||
`;
|
||||
// Added a version number next to "Admin Panel"
|
||||
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>
|
||||
<h3>${adminTitle}</h3>
|
||||
</h3>
|
||||
<form id="adminPanelForm">
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>User Management</legend>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button type="button" id="adminOpenAddUser" class="btn btn-success">Add User</button>
|
||||
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">Remove User</button>
|
||||
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">User Permissions</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>OIDC Configuration</legend>
|
||||
<div class="form-group">
|
||||
<label for="oidcProviderUrl">OIDC Provider URL:</label>
|
||||
<input type="text" id="oidcProviderUrl" class="form-control" value="${window.currentOIDCConfig.providerUrl}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcClientId">OIDC Client ID:</label>
|
||||
<input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcClientSecret">OIDC Client Secret:</label>
|
||||
<input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcRedirectUri">OIDC Redirect URI:</label>
|
||||
<input type="text" id="oidcRedirectUri" class="form-control" value="${window.currentOIDCConfig.redirectUri}" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>Global TOTP Settings</legend>
|
||||
<div class="form-group">
|
||||
<label for="globalOtpauthUrl">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>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>Login Options</legend>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableFormLogin" />
|
||||
<label for="disableFormLogin">Disable Login Form</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableBasicAuth" />
|
||||
<label for="disableBasicAuth">Disable Basic HTTP Auth</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableOIDCLogin" />
|
||||
<label for="disableOIDCLogin">Disable OIDC Login</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" id="saveAdminSettings" class="btn btn-primary">Save Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(adminModal);
|
||||
|
||||
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
||||
adminModal.addEventListener("click", (e) => {
|
||||
if (e.target === adminModal) closeAdminPanel();
|
||||
});
|
||||
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
||||
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);
|
||||
});
|
||||
// New event binding for the User Permissions button:
|
||||
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
|
||||
openUserPermissionsModal();
|
||||
});
|
||||
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
||||
if (totalDisabled === 3) {
|
||||
showToast("At least one login method must remain enabled.");
|
||||
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 newOIDCConfig = {
|
||||
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||
clientId: document.getElementById("oidcClientId").value.trim(),
|
||||
clientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
||||
redirectUri: document.getElementById("oidcRedirectUri").value.trim()
|
||||
};
|
||||
const disableFormLogin = disableFormLoginCheckbox.checked;
|
||||
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
||||
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
||||
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
||||
sendRequest("updateConfig.php", "POST", {
|
||||
oidc: newOIDCConfig,
|
||||
disableFormLogin,
|
||||
disableBasicAuth,
|
||||
disableOIDCLogin,
|
||||
globalOtpauthUrl
|
||||
}, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
showToast("Settings updated successfully.");
|
||||
localStorage.setItem("disableFormLogin", disableFormLogin);
|
||||
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
||||
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
||||
if (typeof window.updateLoginOptionsUI === "function") {
|
||||
window.updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
||||
}
|
||||
closeAdminPanel();
|
||||
} else {
|
||||
showToast("Error updating settings: " + (response.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
});
|
||||
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("At least one login method must remain enabled.");
|
||||
changedCheckbox.checked = false;
|
||||
}
|
||||
}
|
||||
disableFormLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||
disableBasicAuthCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||
disableOIDCLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||
|
||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||
} else {
|
||||
adminModal.style.backgroundColor = overlayBackground;
|
||||
const modalContent = adminModal.querySelector(".modal-content");
|
||||
if (modalContent) {
|
||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
||||
modalContent.style.border = isDarkMode ? "1px solid #444" : "1px solid #ccc";
|
||||
}
|
||||
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl;
|
||||
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
|
||||
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
|
||||
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
|
||||
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise';
|
||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||
adminModal.style.display = "flex";
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
let adminModal = document.getElementById("adminPanelModal");
|
||||
if (adminModal) {
|
||||
adminModal.style.backgroundColor = "rgba(0,0,0,0.5)";
|
||||
const modalContent = adminModal.querySelector(".modal-content");
|
||||
if (modalContent) {
|
||||
modalContent.style.background = "#fff";
|
||||
modalContent.style.color = "#000";
|
||||
modalContent.style.border = "1px solid #ccc";
|
||||
}
|
||||
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl;
|
||||
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
|
||||
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
|
||||
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
|
||||
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise';
|
||||
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
||||
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
||||
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
||||
adminModal.style.display = "flex";
|
||||
} else {
|
||||
openAdminPanel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function closeAdminPanel() {
|
||||
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>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">Cancel</button>
|
||||
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">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("updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
showToast("User permissions updated successfully.");
|
||||
userPermissionsModal.style.display = "none";
|
||||
} else {
|
||||
showToast("Error updating permissions: " + (response.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast("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("getUserPermissions.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(permissionsData => {
|
||||
// Then, fetch the list of users.
|
||||
return fetch("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>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 localStorage defaults.
|
||||
const defaultPerm = {
|
||||
folderOnly: localStorage.getItem("folderOnly") === "true",
|
||||
readOnly: localStorage.getItem("readOnly") === "true",
|
||||
disableUpload: localStorage.getItem("disableUpload") === "true"
|
||||
};
|
||||
const userPerm = (permissionsData && typeof permissionsData === "object" && permissionsData[user.username]) || 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" : ""} />
|
||||
User Folder Only
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="readOnly" ${userPerm.readOnly ? "checked" : ""} />
|
||||
Read Only
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="disableUpload" ${userPerm.disableUpload ? "checked" : ""} />
|
||||
Disable Upload
|
||||
</label>
|
||||
</div>
|
||||
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
|
||||
`;
|
||||
listContainer.appendChild(row);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
listContainer.innerHTML = "<p>Error loading users.</p>";
|
||||
});
|
||||
}
|
||||
@@ -1,480 +0,0 @@
|
||||
// fileListView.js
|
||||
import {
|
||||
escapeHTML,
|
||||
debounce,
|
||||
buildSearchAndPaginationControls,
|
||||
buildFileTableHeader,
|
||||
buildFileTableRow,
|
||||
buildBottomControls,
|
||||
updateFileActionButtons,
|
||||
showToast,
|
||||
updateRowHighlight,
|
||||
toggleRowSelection,
|
||||
attachEnterKeyListener
|
||||
} from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
import { bindFileListContextMenu } from './fileMenu.js';
|
||||
import { openDownloadModal } from './fileActions.js';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
||||
|
||||
export let fileData = [];
|
||||
export let sortOrder = { column: "uploaded", ascending: true };
|
||||
|
||||
window.itemsPerPage = window.itemsPerPage || 10;
|
||||
window.currentPage = window.currentPage || 1;
|
||||
window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "gallery"
|
||||
|
||||
/**
|
||||
* --- Helper Functions ---
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a file size string (e.g. "456.9KB", "1.2 MB", "1024") into bytes.
|
||||
*/
|
||||
function parseSizeToBytes(sizeStr) {
|
||||
if (!sizeStr) return 0;
|
||||
// Remove any whitespace
|
||||
let s = sizeStr.trim();
|
||||
// Extract the numerical part.
|
||||
let value = parseFloat(s);
|
||||
// Determine if there is a unit. Convert the unit to uppercase for easier matching.
|
||||
let upper = s.toUpperCase();
|
||||
if (upper.includes("KB")) {
|
||||
value *= 1024;
|
||||
} else if (upper.includes("MB")) {
|
||||
value *= 1024 * 1024;
|
||||
} else if (upper.includes("GB")) {
|
||||
value *= 1024 * 1024 * 1024;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the total bytes as a human-readable string, choosing an appropriate unit.
|
||||
*/
|
||||
function formatSize(totalBytes) {
|
||||
if (totalBytes < 1024) {
|
||||
return totalBytes + " Bytes";
|
||||
} else if (totalBytes < 1024 * 1024) {
|
||||
return (totalBytes / 1024).toFixed(2) + " KB";
|
||||
} else if (totalBytes < 1024 * 1024 * 1024) {
|
||||
return (totalBytes / (1024 * 1024)).toFixed(2) + " MB";
|
||||
} else {
|
||||
return (totalBytes / (1024 * 1024 * 1024)).toFixed(2) + " GB";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the folder summary HTML using the filtered file list.
|
||||
* This function sums the file sizes in bytes correctly, then formats the total.
|
||||
*/
|
||||
function buildFolderSummary(filteredFiles) {
|
||||
const totalFiles = filteredFiles.length;
|
||||
const totalBytes = filteredFiles.reduce((sum, file) => {
|
||||
// file.size might be something like "456.9KB" or just "1024".
|
||||
return sum + parseSizeToBytes(file.size);
|
||||
}, 0);
|
||||
const sizeStr = formatSize(totalBytes);
|
||||
return `<strong>Total Files:</strong> ${totalFiles} | <strong>Total Size:</strong> ${sizeStr}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* --- VIEW MODE TOGGLE BUTTON & Helpers ---
|
||||
*/
|
||||
export function createViewToggleButton() {
|
||||
let toggleBtn = document.getElementById("toggleViewBtn");
|
||||
if (!toggleBtn) {
|
||||
toggleBtn = document.createElement("button");
|
||||
toggleBtn.id = "toggleViewBtn";
|
||||
toggleBtn.classList.add("btn", "btn-secondary");
|
||||
const titleElem = document.getElementById("fileListTitle");
|
||||
if (titleElem) {
|
||||
titleElem.parentNode.insertBefore(toggleBtn, titleElem.nextSibling);
|
||||
}
|
||||
}
|
||||
toggleBtn.textContent = window.viewMode === "gallery" ? t("switch_to_table_view") : t("switch_to_gallery_view");
|
||||
toggleBtn.onclick = () => {
|
||||
window.viewMode = window.viewMode === "gallery" ? "table" : "gallery";
|
||||
localStorage.setItem("viewMode", window.viewMode);
|
||||
loadFileList(window.currentFolder);
|
||||
toggleBtn.textContent = window.viewMode === "gallery" ? t("switch_to_table_view") : t("switch_to_gallery_view");
|
||||
};
|
||||
return toggleBtn;
|
||||
}
|
||||
|
||||
export function formatFolderName(folder) {
|
||||
if (folder === "root") return "(Root)";
|
||||
return folder
|
||||
.replace(/[_-]+/g, " ")
|
||||
.replace(/\b\w/g, char => char.toUpperCase());
|
||||
}
|
||||
|
||||
// Expose inline DOM helpers.
|
||||
window.toggleRowSelection = toggleRowSelection;
|
||||
window.updateRowHighlight = updateRowHighlight;
|
||||
|
||||
/**
|
||||
* --- FILE LIST & VIEW RENDERING ---
|
||||
*/
|
||||
export function loadFileList(folderParam) {
|
||||
const folder = folderParam || "root";
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
|
||||
fileListContainer.style.visibility = "hidden";
|
||||
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
|
||||
|
||||
return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime())
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
showToast("Session expired. Please log in again.");
|
||||
window.location.href = "logout.php";
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
fileListContainer.innerHTML = ""; // Clear loading message.
|
||||
if (data.files && data.files.length > 0) {
|
||||
data.files = data.files.map(file => {
|
||||
file.fullName = (file.path || file.name).trim().toLowerCase();
|
||||
file.editable = canEditFile(file.name);
|
||||
file.folder = folder;
|
||||
if (!file.type && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
||||
file.type = "image";
|
||||
}
|
||||
return file;
|
||||
});
|
||||
fileData = data.files;
|
||||
|
||||
// Update the file list actions area without removing existing buttons.
|
||||
const actionsContainer = document.getElementById("fileListActions");
|
||||
if (actionsContainer) {
|
||||
let summaryElem = document.getElementById("fileSummary");
|
||||
if (!summaryElem) {
|
||||
summaryElem = document.createElement("div");
|
||||
summaryElem.id = "fileSummary";
|
||||
summaryElem.style.float = "right";
|
||||
summaryElem.style.marginLeft = "auto";
|
||||
summaryElem.style.marginRight = "60px";
|
||||
summaryElem.style.fontSize = "0.9em";
|
||||
actionsContainer.appendChild(summaryElem);
|
||||
} else {
|
||||
summaryElem.style.display = "block";
|
||||
}
|
||||
summaryElem.innerHTML = buildFolderSummary(fileData);
|
||||
}
|
||||
|
||||
// Render the view normally.
|
||||
if (window.viewMode === "gallery") {
|
||||
renderGalleryView(folder);
|
||||
} else {
|
||||
renderFileTable(folder);
|
||||
}
|
||||
} else {
|
||||
fileListContainer.textContent = t("no_files_found");
|
||||
const summaryElem = document.getElementById("fileSummary");
|
||||
if (summaryElem) {
|
||||
summaryElem.style.display = "none";
|
||||
}
|
||||
updateFileActionButtons();
|
||||
}
|
||||
return data.files || [];
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error loading file list:", error);
|
||||
if (error.message !== "Unauthorized") {
|
||||
fileListContainer.textContent = "Error loading files.";
|
||||
}
|
||||
return [];
|
||||
})
|
||||
.finally(() => {
|
||||
fileListContainer.style.visibility = "visible";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update renderFileTable so that it writes its content into the provided container.
|
||||
* If no container is provided, it defaults to the element with id "fileList".
|
||||
*/
|
||||
export function renderFileTable(folder, container) {
|
||||
const fileListContent = container || document.getElementById("fileList");
|
||||
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
||||
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
|
||||
let currentPage = window.currentPage || 1;
|
||||
|
||||
const filteredFiles = fileData.filter(file => {
|
||||
const nameMatch = file.name.toLowerCase().includes(searchTerm);
|
||||
const tagMatch = file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm));
|
||||
return nameMatch || tagMatch;
|
||||
});
|
||||
const totalFiles = filteredFiles.length;
|
||||
const totalPages = Math.ceil(totalFiles / itemsPerPageSetting);
|
||||
if (currentPage > totalPages) {
|
||||
currentPage = totalPages > 0 ? totalPages : 1;
|
||||
window.currentPage = currentPage;
|
||||
}
|
||||
const folderPath = folder === "root"
|
||||
? "uploads/"
|
||||
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
||||
|
||||
const topControlsHTML = buildSearchAndPaginationControls({
|
||||
currentPage,
|
||||
totalPages,
|
||||
searchTerm: window.currentSearchTerm || ""
|
||||
});
|
||||
let headerHTML = buildFileTableHeader(sortOrder);
|
||||
const startIndex = (currentPage - 1) * itemsPerPageSetting;
|
||||
const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles);
|
||||
let rowsHTML = "<tbody>";
|
||||
if (totalFiles > 0) {
|
||||
filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => {
|
||||
let rowHTML = buildFileTableRow(file, folderPath);
|
||||
rowHTML = rowHTML.replace("<tr", `<tr id="file-row-${encodeURIComponent(file.name)}-${startIndex + idx}"`);
|
||||
|
||||
let tagBadgesHTML = "";
|
||||
if (file.tags && file.tags.length > 0) {
|
||||
tagBadgesHTML = '<div class="tag-badges" style="display:inline-block; margin-left:5px;">';
|
||||
file.tags.forEach(tag => {
|
||||
tagBadgesHTML += `<span style="background-color: ${tag.color}; color: #fff; padding: 2px 4px; border-radius: 3px; margin-right: 2px; font-size: 0.8em;">${escapeHTML(tag.name)}</span>`;
|
||||
});
|
||||
tagBadgesHTML += "</div>";
|
||||
}
|
||||
rowHTML = rowHTML.replace(/(<td class="file-name-cell">)(.*?)(<\/td>)/, (match, p1, p2, p3) => {
|
||||
return p1 + p2 + tagBadgesHTML + p3;
|
||||
});
|
||||
rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `<button class="share-btn btn btn-sm btn-secondary" data-file="${escapeHTML(file.name)}" title="Share">
|
||||
<i class="material-icons">share</i>
|
||||
</button>$1`);
|
||||
rowsHTML += rowHTML;
|
||||
});
|
||||
} else {
|
||||
rowsHTML += `<tr><td colspan="8">No files found.</td></tr>`;
|
||||
}
|
||||
rowsHTML += "</tbody></table>";
|
||||
const bottomControlsHTML = buildBottomControls(itemsPerPageSetting);
|
||||
|
||||
fileListContent.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||
|
||||
createViewToggleButton();
|
||||
|
||||
// Setup event listeners as before...
|
||||
const newSearchInput = document.getElementById("searchInput");
|
||||
if (newSearchInput) {
|
||||
newSearchInput.addEventListener("input", debounce(function () {
|
||||
window.currentSearchTerm = newSearchInput.value;
|
||||
window.currentPage = 1;
|
||||
renderFileTable(folder, container);
|
||||
setTimeout(() => {
|
||||
const freshInput = document.getElementById("searchInput");
|
||||
if (freshInput) {
|
||||
freshInput.focus();
|
||||
const len = freshInput.value.length;
|
||||
freshInput.setSelectionRange(len, len);
|
||||
}
|
||||
}, 0);
|
||||
}, 300));
|
||||
}
|
||||
document.querySelectorAll("table.table thead th[data-column]").forEach(cell => {
|
||||
cell.addEventListener("click", function () {
|
||||
const column = this.getAttribute("data-column");
|
||||
sortFiles(column, folder);
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("#fileList .file-checkbox").forEach(checkbox => {
|
||||
checkbox.addEventListener("change", function (e) {
|
||||
updateRowHighlight(e.target);
|
||||
updateFileActionButtons();
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".share-btn").forEach(btn => {
|
||||
btn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
const fileName = this.getAttribute("data-file");
|
||||
const file = fileData.find(f => f.name === fileName);
|
||||
if (file) {
|
||||
import('./filePreview.js').then(module => {
|
||||
module.openShareModal(file, folder);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
updateFileActionButtons();
|
||||
document.querySelectorAll("#fileListContent tbody tr").forEach(row => {
|
||||
row.setAttribute("draggable", "true");
|
||||
import('./fileDragDrop.js').then(module => {
|
||||
row.addEventListener("dragstart", module.fileDragStartHandler);
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".download-btn, .edit-btn, .rename-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => e.stopPropagation());
|
||||
});
|
||||
bindFileListContextMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Similarly, update renderGalleryView to accept an optional container.
|
||||
*/
|
||||
export function renderGalleryView(folder, container) {
|
||||
const fileListContent = container || document.getElementById("fileList");
|
||||
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
||||
const filteredFiles = fileData.filter(file => {
|
||||
return file.name.toLowerCase().includes(searchTerm) ||
|
||||
(file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm)));
|
||||
});
|
||||
const folderPath = folder === "root"
|
||||
? "uploads/"
|
||||
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
||||
const gridStyle = "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; padding: 10px;";
|
||||
let galleryHTML = `<div class="gallery-container" style="${gridStyle}">`;
|
||||
filteredFiles.forEach((file) => {
|
||||
let thumbnail;
|
||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
||||
thumbnail = `<img src="${folderPath + encodeURIComponent(file.name)}?t=${new Date().getTime()}" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: 150px; display: block; margin: 0 auto;">`;
|
||||
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
|
||||
} else {
|
||||
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
|
||||
}
|
||||
let tagBadgesHTML = "";
|
||||
if (file.tags && file.tags.length > 0) {
|
||||
tagBadgesHTML = `<div class="tag-badges" style="margin-top:4px;">`;
|
||||
file.tags.forEach(tag => {
|
||||
tagBadgesHTML += `<span style="background-color: ${tag.color}; color: #fff; padding: 2px 4px; border-radius: 3px; margin-right: 2px; font-size: 0.8em;">${escapeHTML(tag.name)}</span>`;
|
||||
});
|
||||
tagBadgesHTML += `</div>`;
|
||||
}
|
||||
galleryHTML += `<div class="gallery-card" style="border: 1px solid #ccc; padding: 5px; text-align: center;">
|
||||
<div class="gallery-preview" style="cursor: pointer;" onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t=' + new Date().getTime(), '${file.name}')">
|
||||
${thumbnail}
|
||||
</div>
|
||||
<div class="gallery-info" style="margin-top: 5px;">
|
||||
<span class="gallery-file-name" style="display: block;">${escapeHTML(file.name)}</span>
|
||||
${tagBadgesHTML}
|
||||
<div class="button-wrap" style="display: flex; justify-content: center; gap: 5px;">
|
||||
<button type="button" class="btn btn-sm btn-success download-btn"
|
||||
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
|
||||
title="Download">
|
||||
<i class="material-icons">file_download</i>
|
||||
</button>
|
||||
${file.editable ? `
|
||||
<button class="btn btn-sm edit-btn" onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="Edit">
|
||||
<i class="material-icons">edit</i>
|
||||
</button>
|
||||
` : ""}
|
||||
<button class="btn btn-sm btn-warning rename-btn" onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="Rename">
|
||||
<i class="material-icons">drive_file_rename_outline</i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary share-btn" data-file="${escapeHTML(file.name)}" title="Share">
|
||||
<i class="material-icons">share</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
galleryHTML += "</div>";
|
||||
fileListContent.innerHTML = galleryHTML;
|
||||
createViewToggleButton();
|
||||
updateFileActionButtons();
|
||||
document.querySelectorAll(".share-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
const fileName = btn.getAttribute("data-file");
|
||||
const file = fileData.find(f => f.name === fileName);
|
||||
import('./filePreview.js').then(module => {
|
||||
module.openShareModal(file, folder);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function sortFiles(column, folder) {
|
||||
if (sortOrder.column === column) {
|
||||
sortOrder.ascending = !sortOrder.ascending;
|
||||
} else {
|
||||
sortOrder.column = column;
|
||||
sortOrder.ascending = true;
|
||||
}
|
||||
fileData.sort((a, b) => {
|
||||
let valA = a[column] || "";
|
||||
let valB = b[column] || "";
|
||||
if (column === "modified" || column === "uploaded") {
|
||||
const parsedA = parseCustomDate(valA);
|
||||
const parsedB = parseCustomDate(valB);
|
||||
valA = parsedA;
|
||||
valB = parsedB;
|
||||
} else if (typeof valA === "string") {
|
||||
valA = valA.toLowerCase();
|
||||
valB = valB.toLowerCase();
|
||||
}
|
||||
if (valA < valB) return sortOrder.ascending ? -1 : 1;
|
||||
if (valA > valB) return sortOrder.ascending ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
if (window.viewMode === "gallery") {
|
||||
renderGalleryView(folder);
|
||||
} else {
|
||||
renderFileTable(folder);
|
||||
}
|
||||
}
|
||||
|
||||
function parseCustomDate(dateStr) {
|
||||
dateStr = dateStr.replace(/\s+/g, " ").trim();
|
||||
const parts = dateStr.split(" ");
|
||||
if (parts.length !== 2) {
|
||||
return new Date(dateStr).getTime();
|
||||
}
|
||||
const datePart = parts[0];
|
||||
const timePart = parts[1];
|
||||
const dateComponents = datePart.split("/");
|
||||
if (dateComponents.length !== 3) {
|
||||
return new Date(dateStr).getTime();
|
||||
}
|
||||
let month = parseInt(dateComponents[0], 10);
|
||||
let day = parseInt(dateComponents[1], 10);
|
||||
let year = parseInt(dateComponents[2], 10);
|
||||
if (year < 100) {
|
||||
year += 2000;
|
||||
}
|
||||
const timeRegex = /^(\d{1,2}):(\d{2})(AM|PM)$/i;
|
||||
const match = timePart.match(timeRegex);
|
||||
if (!match) {
|
||||
return new Date(dateStr).getTime();
|
||||
}
|
||||
let hour = parseInt(match[1], 10);
|
||||
const minute = parseInt(match[2], 10);
|
||||
const period = match[3].toUpperCase();
|
||||
if (period === "PM" && hour !== 12) {
|
||||
hour += 12;
|
||||
}
|
||||
if (period === "AM" && hour === 12) {
|
||||
hour = 0;
|
||||
}
|
||||
return new Date(year, month - 1, day, hour, minute).getTime();
|
||||
}
|
||||
|
||||
export function canEditFile(fileName) {
|
||||
const allowedExtensions = [
|
||||
"txt", "html", "htm", "css", "js", "json", "xml",
|
||||
"md", "py", "ini", "csv", "log", "conf", "config", "bat",
|
||||
"rtf", "doc", "docx"
|
||||
];
|
||||
const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||
return allowedExtensions.includes(ext);
|
||||
}
|
||||
|
||||
// Expose global functions for pagination and preview.
|
||||
window.changePage = function (newPage) {
|
||||
window.currentPage = newPage;
|
||||
renderFileTable(window.currentFolder);
|
||||
};
|
||||
window.changeItemsPerPage = function (newCount) {
|
||||
window.itemsPerPage = parseInt(newCount);
|
||||
window.currentPage = 1;
|
||||
renderFileTable(window.currentFolder);
|
||||
};
|
||||
|
||||
// fileListView.js (bottom)
|
||||
window.loadFileList = loadFileList;
|
||||
window.renderFileTable = renderFileTable;
|
||||
window.renderGalleryView = renderGalleryView;
|
||||
window.sortFiles = sortFiles;
|
||||
@@ -1,270 +0,0 @@
|
||||
// filePreview.js
|
||||
import { escapeHTML, showToast } from './domUtils.js';
|
||||
import { fileData } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
export function openShareModal(file, folder) {
|
||||
const existing = document.getElementById("shareModal");
|
||||
if (existing) existing.remove();
|
||||
|
||||
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-header">
|
||||
<h3>${t("share_file")}: ${escapeHTML(file.name)}</h3>
|
||||
<span class="close-image-modal" id="closeShareModal" title="Close">×</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>
|
||||
<p>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;">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = "block";
|
||||
|
||||
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("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
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.token) {
|
||||
let shareEndpoint = document.querySelector('meta[name="share-url"]')
|
||||
? document.querySelector('meta[name="share-url"]').getAttribute('content')
|
||||
: (window.SHARE_URL || "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";
|
||||
} else {
|
||||
showToast("Error generating share link: " + (data.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error generating share link:", err);
|
||||
showToast("Error generating share link.");
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("copyShareLinkBtn").addEventListener("click", () => {
|
||||
const input = document.getElementById("shareLinkInput");
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
showToast("Link copied to clipboard!");
|
||||
});
|
||||
}
|
||||
|
||||
export function previewFile(fileUrl, fileName) {
|
||||
let modal = document.getElementById("filePreviewModal");
|
||||
if (!modal) {
|
||||
modal = document.createElement("div");
|
||||
modal.id = "filePreviewModal";
|
||||
Object.assign(modal.style, {
|
||||
position: "fixed",
|
||||
top: "0",
|
||||
left: "0",
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
backgroundColor: "rgba(0,0,0,0.7)",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: "1000"
|
||||
});
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content image-preview-modal-content" style="position: relative; max-width: 90vw; max-height: 90vh;">
|
||||
<span id="closeFileModal" class="close-image-modal" style="position: absolute; top: 10px; right: 10px; font-size: 24px; cursor: pointer;">×</span>
|
||||
<h4 class="image-modal-header"></h4>
|
||||
<div class="file-preview-container" style="position: relative; text-align: center;"></div>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
function closeModal() {
|
||||
const mediaElements = modal.querySelectorAll("video, audio");
|
||||
mediaElements.forEach(media => {
|
||||
media.pause();
|
||||
if (media.tagName.toLowerCase() !== 'video') {
|
||||
try {
|
||||
media.currentTime = 0;
|
||||
} catch(e) { }
|
||||
}
|
||||
});
|
||||
modal.style.display = "none";
|
||||
}
|
||||
|
||||
document.getElementById("closeFileModal").addEventListener("click", closeModal);
|
||||
modal.addEventListener("click", function (e) {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
modal.querySelector("h4").textContent = fileName;
|
||||
const container = modal.querySelector(".file-preview-container");
|
||||
container.innerHTML = "";
|
||||
|
||||
const extension = fileName.split('.').pop().toLowerCase();
|
||||
const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(fileName);
|
||||
if (isImage) {
|
||||
const img = document.createElement("img");
|
||||
img.src = fileUrl;
|
||||
img.className = "image-modal-img";
|
||||
img.style.maxWidth = "80vw";
|
||||
img.style.maxHeight = "80vh";
|
||||
container.appendChild(img);
|
||||
|
||||
const images = fileData.filter(file => /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name));
|
||||
if (images.length > 1) {
|
||||
modal.galleryImages = images;
|
||||
modal.galleryCurrentIndex = images.findIndex(f => f.name === fileName);
|
||||
|
||||
const prevBtn = document.createElement("button");
|
||||
prevBtn.textContent = "‹";
|
||||
prevBtn.className = "gallery-nav-btn";
|
||||
prevBtn.style.cssText = "position: absolute; top: 50%; left: 10px; transform: translateY(-50%); background: transparent; border: none; color: white; font-size: 48px; cursor: pointer;";
|
||||
prevBtn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
modal.galleryCurrentIndex = (modal.galleryCurrentIndex - 1 + modal.galleryImages.length) % modal.galleryImages.length;
|
||||
let newFile = modal.galleryImages[modal.galleryCurrentIndex];
|
||||
modal.querySelector("h4").textContent = newFile.name;
|
||||
img.src = ((window.currentFolder === "root")
|
||||
? "uploads/"
|
||||
: "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/")
|
||||
+ encodeURIComponent(newFile.name) + "?t=" + new Date().getTime();
|
||||
});
|
||||
const nextBtn = document.createElement("button");
|
||||
nextBtn.textContent = "›";
|
||||
nextBtn.className = "gallery-nav-btn";
|
||||
nextBtn.style.cssText = "position: absolute; top: 50%; right: 10px; transform: translateY(-50%); background: transparent; border: none; color: white; font-size: 48px; cursor: pointer;";
|
||||
nextBtn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
modal.galleryCurrentIndex = (modal.galleryCurrentIndex + 1) % modal.galleryImages.length;
|
||||
let newFile = modal.galleryImages[modal.galleryCurrentIndex];
|
||||
modal.querySelector("h4").textContent = newFile.name;
|
||||
img.src = ((window.currentFolder === "root")
|
||||
? "uploads/"
|
||||
: "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/")
|
||||
+ encodeURIComponent(newFile.name) + "?t=" + new Date().getTime();
|
||||
});
|
||||
container.appendChild(prevBtn);
|
||||
container.appendChild(nextBtn);
|
||||
}
|
||||
} else {
|
||||
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)) {
|
||||
const video = document.createElement("video");
|
||||
video.src = fileUrl;
|
||||
video.controls = true;
|
||||
video.className = "image-modal-img";
|
||||
|
||||
const progressKey = 'videoProgress-' + fileUrl;
|
||||
|
||||
video.addEventListener("loadedmetadata", () => {
|
||||
const savedTime = localStorage.getItem(progressKey);
|
||||
if (savedTime) {
|
||||
video.currentTime = parseFloat(savedTime);
|
||||
}
|
||||
});
|
||||
|
||||
video.addEventListener("timeupdate", () => {
|
||||
localStorage.setItem(progressKey, video.currentTime);
|
||||
});
|
||||
|
||||
video.addEventListener("ended", () => {
|
||||
localStorage.removeItem(progressKey);
|
||||
});
|
||||
|
||||
container.appendChild(video);
|
||||
|
||||
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(fileName)) {
|
||||
const audio = document.createElement("audio");
|
||||
audio.src = fileUrl;
|
||||
audio.controls = true;
|
||||
audio.className = "audio-modal";
|
||||
audio.style.maxWidth = "80vw";
|
||||
container.appendChild(audio);
|
||||
} else {
|
||||
container.textContent = "Preview not available for this file type.";
|
||||
}
|
||||
}
|
||||
modal.style.display = "flex";
|
||||
}
|
||||
|
||||
// Added to preserve the original functionality.
|
||||
export function displayFilePreview(file, container) {
|
||||
const actualFile = file.file || file;
|
||||
|
||||
// Validate that actualFile is indeed a File
|
||||
if (!(actualFile instanceof File)) {
|
||||
console.error("displayFilePreview called with an invalid file object");
|
||||
return;
|
||||
}
|
||||
|
||||
container.style.display = "inline-block";
|
||||
|
||||
// Clear the container safely without using innerHTML
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
|
||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(actualFile.name)) {
|
||||
const img = document.createElement("img");
|
||||
// Set the image source using a Blob URL (this is considered safe)
|
||||
img.src = URL.createObjectURL(actualFile);
|
||||
img.classList.add("file-preview-img");
|
||||
container.appendChild(img);
|
||||
} else {
|
||||
const iconSpan = document.createElement("span");
|
||||
iconSpan.classList.add("material-icons", "file-icon");
|
||||
iconSpan.textContent = "insert_drive_file";
|
||||
container.appendChild(iconSpan);
|
||||
}
|
||||
}
|
||||
|
||||
window.previewFile = previewFile;
|
||||
window.openShareModal = openShareModal;
|
||||
@@ -1,107 +0,0 @@
|
||||
// folderShareModal.js
|
||||
import { escapeHTML, showToast } from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
export function openFolderShareModal(folder) {
|
||||
// Remove any existing folder share modal
|
||||
const existing = document.getElementById("folderShareModal");
|
||||
if (existing) existing.remove();
|
||||
|
||||
// Create the modal container
|
||||
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-header">
|
||||
<h3>${t("share_folder")}: ${escapeHTML(folder)}</h3>
|
||||
<span class="close-image-modal" id="closeFolderShareModal" title="Close">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>${t("set_expiration")}</p>
|
||||
<select id="folderShareExpiration">
|
||||
<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>
|
||||
</select>
|
||||
<p>${t("password_optional")}</p>
|
||||
<input type="text" id="folderSharePassword" placeholder="${t("password")}" style="width: 100%;"/>
|
||||
<br>
|
||||
<label>
|
||||
<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;">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = "block";
|
||||
|
||||
// Close button handler
|
||||
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("/createFolderShareLink.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
|
||||
})
|
||||
})
|
||||
.then(response => response.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";
|
||||
showToast(t("share_link_generated"));
|
||||
} else {
|
||||
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")));
|
||||
});
|
||||
});
|
||||
|
||||
// Copy share link button handler
|
||||
document.getElementById("copyFolderShareLinkBtn").addEventListener("click", () => {
|
||||
const input = document.getElementById("folderShareLinkInput");
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
showToast(t("link_copied"));
|
||||
});
|
||||
}
|
||||
611
js/i18n.js
611
js/i18n.js
@@ -1,611 +0,0 @@
|
||||
/* i18n.js */
|
||||
const translations = {
|
||||
en: { /* English translations */
|
||||
"please_log_in_to_continue": "Please log in to continue.",
|
||||
"no_files_selected": "No files selected.",
|
||||
"confirm_delete_files": "Are you sure you want to delete {count} selected file(s)?",
|
||||
"element_not_found": "Element with id \"{id}\" not found.",
|
||||
"search_placeholder": "Search files or tag...",
|
||||
"file_name": "File Name",
|
||||
"date_modified": "Date Modified",
|
||||
"upload_date": "Upload Date",
|
||||
"file_size": "File Size",
|
||||
"uploader": "Uploader",
|
||||
"enter_totp_code": "Enter TOTP Code",
|
||||
"use_recovery_code_instead": "Use Recovery Code instead",
|
||||
"enter_recovery_code": "Enter Recovery Code",
|
||||
"editing": "Editing",
|
||||
"decrease_font": "A-",
|
||||
"increase_font": "A+",
|
||||
"save": "Save",
|
||||
"close": "Close",
|
||||
"no_files_found": "No files found.",
|
||||
"switch_to_table_view": "Switch to Table View",
|
||||
"switch_to_gallery_view": "Switch to Gallery View",
|
||||
"share_file": "Share File",
|
||||
"set_expiration": "Set Expiration:",
|
||||
"password_optional": "Password (optional):",
|
||||
"generate_share_link": "Generate Share Link",
|
||||
"shareable_link": "Shareable Link:",
|
||||
"copy_link": "Copy Link",
|
||||
"tag_file": "Tag File",
|
||||
"tag_name": "Tag Name:",
|
||||
"tag_color": "Tag Color:",
|
||||
"save_tag": "Save Tag",
|
||||
"files_in": "Files in",
|
||||
"light_mode": "Light Mode",
|
||||
"dark_mode": "Dark Mode",
|
||||
"upload_instruction": "Drop files/folders here or click 'Choose files'",
|
||||
"no_files_selected_default": "No files selected",
|
||||
"choose_files": "Choose files",
|
||||
"delete_selected": "Delete Selected",
|
||||
"copy_selected": "Copy Selected",
|
||||
"move_selected": "Move Selected",
|
||||
"tag_selected": "Tag Selected",
|
||||
"download_zip": "Download Zip",
|
||||
"extract_zip": "Extract Zip",
|
||||
"preview": "Preview",
|
||||
"edit": "Edit",
|
||||
"rename": "Rename",
|
||||
"trash_empty": "Trash is empty.",
|
||||
"no_trash_selected": "No trash items selected for restore.",
|
||||
|
||||
// Additional keys for HTML translations:
|
||||
"title": "FileRise",
|
||||
"header_title": "FileRise",
|
||||
"logout": "Logout",
|
||||
"change_password": "Change Password",
|
||||
"restore_text": "Restore or",
|
||||
"delete_text": "Delete Trash Items",
|
||||
"restore_selected": "Restore Selected",
|
||||
"restore_all": "Restore All",
|
||||
"delete_selected_trash": "Delete Selected",
|
||||
"delete_all": "Delete All",
|
||||
"upload_header": "Upload Files/Folders",
|
||||
|
||||
// Folder Management keys:
|
||||
"folder_navigation": "Folder Navigation & Management",
|
||||
"create_folder": "Create Folder",
|
||||
"create_folder_title": "Create Folder",
|
||||
"enter_folder_name": "Enter folder name",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"rename_folder": "Rename Folder",
|
||||
"rename_folder_title": "Rename Folder",
|
||||
"rename_folder_placeholder": "Enter new folder name",
|
||||
"delete_folder": "Delete Folder",
|
||||
"delete_folder_title": "Delete Folder",
|
||||
"delete_folder_message": "Are you sure you want to delete this folder?",
|
||||
"folder_help": "Folder Help",
|
||||
"folder_help_item_1": "Click on a folder in the tree to view its files.",
|
||||
"folder_help_item_2": "Use [-] to collapse and [+] to expand folders.",
|
||||
"folder_help_item_3": "Select a folder and click \"Create Folder\" to add a subfolder.",
|
||||
"folder_help_item_4": "To rename or delete a folder, select it and then click the appropriate button.",
|
||||
|
||||
// File List keys:
|
||||
"file_list_title": "Files in (Root)",
|
||||
"files_in": "Files in",
|
||||
"delete_files": "Delete Files",
|
||||
"delete_selected_files_title": "Delete Selected Files",
|
||||
"delete_files_message": "Are you sure you want to delete the selected files?",
|
||||
"copy_files": "Copy Files",
|
||||
"copy_files_title": "Copy Selected Files",
|
||||
"copy_files_message": "Select a target folder for copying the selected files:",
|
||||
"move_files": "Move Files",
|
||||
"move_files_title": "Move Selected Files",
|
||||
"move_files_message": "Select a target folder for moving the selected files:",
|
||||
"move": "Move",
|
||||
"extract_zip_button": "Extract Zip",
|
||||
"download_zip_title": "Download Selected Files as Zip",
|
||||
"download_zip_prompt": "Enter a name for the zip file:",
|
||||
"zip_placeholder": "files.zip",
|
||||
|
||||
// Login Form keys:
|
||||
"login": "Login",
|
||||
"remember_me": "Remember me",
|
||||
"login_oidc": "Login with OIDC",
|
||||
"basic_http_login": "Use Basic HTTP Login",
|
||||
|
||||
// Change Password keys:
|
||||
"change_password_title": "Change Password",
|
||||
"old_password": "Old Password",
|
||||
"new_password": "New Password",
|
||||
"confirm_new_password": "Confirm New Password",
|
||||
|
||||
// Add User keys:
|
||||
"create_new_user_title": "Create New User",
|
||||
"username": "Username:",
|
||||
"password": "Password:",
|
||||
"grant_admin": "Grant Admin Access",
|
||||
"save_user": "Save User",
|
||||
|
||||
// Remove User keys:
|
||||
"remove_user_title": "Remove User",
|
||||
"select_user_remove": "Select a user to remove:",
|
||||
"delete_user": "Delete User",
|
||||
|
||||
// Rename File keys:
|
||||
"rename_file_title": "Rename File",
|
||||
"rename_file_placeholder": "Enter new file name",
|
||||
|
||||
// Folder Share
|
||||
"share_folder": "Share Folder",
|
||||
"allow_uploads": "Allow Uploads",
|
||||
"share_link_generated": "Share Link Generated",
|
||||
"error_generating_share_link": "Error Generating Share Link",
|
||||
|
||||
// Folder
|
||||
"create_folder": "Create Folder",
|
||||
"rename_folder": "Rename Folder",
|
||||
"folder_share": "Share Folder",
|
||||
"delete_folder": "Delete Folder",
|
||||
|
||||
// Custom Confirm Modal keys:
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"delete": "Delete",
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"copy": "Copy",
|
||||
"extract": "Extract",
|
||||
"user": "User:",
|
||||
"unknown_error": "Unknown Error",
|
||||
"link_copied": "Link Copied to Clipboard",
|
||||
"minutes": "minutes",
|
||||
"hours": "hours",
|
||||
"days": "days",
|
||||
"weeks": "weeks",
|
||||
"months": "months",
|
||||
"seconds": "seconds",
|
||||
|
||||
// Dark Mode Toggle
|
||||
"dark_mode_toggle": "Dark Mode",
|
||||
"light_mode_toggle": "Light Mode"
|
||||
},
|
||||
es: { /* Spanish translations */
|
||||
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
|
||||
"no_files_selected": "No se seleccionaron archivos.",
|
||||
"confirm_delete_files": "¿Está seguro de que desea eliminar {count} archivo(s) seleccionado(s)?",
|
||||
"element_not_found": "Elemento con id \"{id}\" no encontrado.",
|
||||
"search_placeholder": "Buscar archivos o etiqueta...",
|
||||
"file_name": "Nombre del archivo",
|
||||
"date_modified": "Fecha de modificación",
|
||||
"upload_date": "Fecha de carga",
|
||||
"file_size": "Tamaño del archivo",
|
||||
"uploader": "Cargado por",
|
||||
"enter_totp_code": "Ingrese el código TOTP",
|
||||
"use_recovery_code_instead": "Usar código de recuperación en su lugar",
|
||||
"enter_recovery_code": "Ingrese el código de recuperación",
|
||||
"editing": "Editando",
|
||||
"decrease_font": "A-",
|
||||
"increase_font": "A+",
|
||||
"save": "Guardar",
|
||||
"close": "Cerrar",
|
||||
"no_files_found": "No se encontraron archivos.",
|
||||
"switch_to_table_view": "Cambiar a vista de tabla",
|
||||
"switch_to_gallery_view": "Cambiar a vista de galería",
|
||||
"share_file": "Compartir archivo",
|
||||
"set_expiration": "Establecer vencimiento:",
|
||||
"password_optional": "Contraseña (opcional):",
|
||||
"generate_share_link": "Generar enlace para compartir",
|
||||
"shareable_link": "Enlace para compartir:",
|
||||
"copy_link": "Copiar enlace",
|
||||
"tag_file": "Etiquetar archivo",
|
||||
"tag_name": "Nombre de la etiqueta:",
|
||||
"tag_color": "Color de la etiqueta:",
|
||||
"save_tag": "Guardar etiqueta",
|
||||
"files_in": "Archivos en",
|
||||
"light_mode": "Modo claro",
|
||||
"dark_mode": "Modo oscuro",
|
||||
"upload_instruction": "Suelte archivos/carpetas o haga clic en 'Elegir archivos'",
|
||||
"no_files_selected_default": "No se seleccionaron archivos",
|
||||
"choose_files": "Elegir archivos",
|
||||
"delete_selected": "Eliminar seleccionados",
|
||||
"copy_selected": "Copiar seleccionados",
|
||||
"move_selected": "Mover seleccionados",
|
||||
"tag_selected": "Etiquetar seleccionados",
|
||||
"download_zip": "Descargar Zip",
|
||||
"extract_zip": "Extraer Zip",
|
||||
"preview": "Vista previa",
|
||||
"edit": "Editar",
|
||||
"rename": "Renombrar",
|
||||
"trash_empty": "La papelera está vacía.",
|
||||
"no_trash_selected": "No se seleccionaron elementos de la papelera para restaurar.",
|
||||
|
||||
// Additional keys for HTML translations:
|
||||
"title": "FileRise",
|
||||
"header_title": "FileRise",
|
||||
"logout": "Cerrar sesión",
|
||||
"change_password": "Cambiar contraseña",
|
||||
"restore_text": "Restaurar o",
|
||||
"delete_text": "Eliminar elementos de la papelera",
|
||||
"restore_selected": "Restaurar seleccionados",
|
||||
"restore_all": "Restaurar todo",
|
||||
"delete_selected_trash": "Eliminar seleccionados",
|
||||
"delete_all": "Eliminar todo",
|
||||
"upload_header": "Cargar archivos/carpetas",
|
||||
|
||||
// Folder Management keys:
|
||||
"folder_navigation": "Navegación y gestión de carpetas",
|
||||
"create_folder": "Crear carpeta",
|
||||
"create_folder_title": "Crear carpeta",
|
||||
"enter_folder_name": "Ingrese el nombre de la carpeta",
|
||||
"cancel": "Cancelar",
|
||||
"create": "Crear",
|
||||
"rename_folder": "Renombrar carpeta",
|
||||
"rename_folder_title": "Renombrar carpeta",
|
||||
"rename_folder_placeholder": "Ingrese el nuevo nombre de la carpeta",
|
||||
"delete_folder": "Eliminar carpeta",
|
||||
"delete_folder_title": "Eliminar carpeta",
|
||||
"delete_folder_message": "¿Está seguro de que desea eliminar esta carpeta?",
|
||||
"folder_help": "Ayuda de carpetas",
|
||||
"folder_help_item_1": "Haga clic en una carpeta en el árbol para ver sus archivos.",
|
||||
"folder_help_item_2": "Utilice [-] para colapsar y [+] para expandir carpetas.",
|
||||
"folder_help_item_3": "Seleccione una carpeta y haga clic en \"Crear carpeta\" para agregar una subcarpeta.",
|
||||
"folder_help_item_4": "Para renombrar o eliminar una carpeta, selecciónela y luego haga clic en el botón correspondiente.",
|
||||
|
||||
// File List keys:
|
||||
"file_list_title": "Archivos en (Raíz)",
|
||||
"delete_files": "Eliminar archivos",
|
||||
"delete_selected_files_title": "Eliminar archivos seleccionados",
|
||||
"delete_files_message": "¿Está seguro de que desea eliminar los archivos seleccionados?",
|
||||
"copy_files": "Copiar archivos",
|
||||
"copy_files_title": "Copiar archivos seleccionados",
|
||||
"copy_files_message": "Seleccione una carpeta destino para copiar los archivos seleccionados:",
|
||||
"move_files": "Mover archivos",
|
||||
"move_files_title": "Mover archivos seleccionados",
|
||||
"move_files_message": "Seleccione una carpeta destino para mover los archivos seleccionados:",
|
||||
"move": "Mover",
|
||||
"extract_zip_button": "Extraer Zip",
|
||||
"download_zip_title": "Descargar archivos seleccionados en Zip",
|
||||
"download_zip_prompt": "Ingrese un nombre para el archivo Zip:",
|
||||
"zip_placeholder": "archivos.zip",
|
||||
|
||||
// Login Form keys:
|
||||
"login": "Iniciar sesión",
|
||||
"remember_me": "Recuérdame",
|
||||
"login_oidc": "Iniciar sesión con OIDC",
|
||||
"basic_http_login": "Usar autenticación HTTP básica",
|
||||
|
||||
// Change Password keys:
|
||||
"change_password_title": "Cambiar contraseña",
|
||||
"old_password": "Contraseña antigua",
|
||||
"new_password": "Nueva contraseña",
|
||||
"confirm_new_password": "Confirmar nueva contraseña",
|
||||
|
||||
// Add User keys:
|
||||
"create_new_user_title": "Crear nuevo usuario",
|
||||
"username": "Usuario:",
|
||||
"password": "Contraseña:",
|
||||
"grant_admin": "Otorgar acceso de administrador",
|
||||
"save_user": "Guardar usuario",
|
||||
|
||||
// Remove User keys:
|
||||
"remove_user_title": "Eliminar usuario",
|
||||
"select_user_remove": "Seleccione un usuario para eliminar:",
|
||||
"delete_user": "Eliminar usuario",
|
||||
|
||||
// Rename File keys:
|
||||
"rename_file_title": "Renombrar archivo",
|
||||
"rename_file_placeholder": "Ingrese el nuevo nombre del archivo",
|
||||
|
||||
// Custom Confirm Modal keys:
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"delete": "Eliminar",
|
||||
"download": "Descargar",
|
||||
"upload": "Cargar",
|
||||
"copy": "Copiar",
|
||||
"extract": "Extraer",
|
||||
|
||||
// Dark Mode Toggle
|
||||
"dark_mode_toggle": "Modo oscuro"
|
||||
},
|
||||
fr: { /* French translations */
|
||||
"please_log_in_to_continue": "Veuillez vous connecter pour continuer.",
|
||||
"no_files_selected": "Aucun fichier sélectionné.",
|
||||
"confirm_delete_files": "Êtes-vous sûr de vouloir supprimer {count} fichier(s) sélectionné(s) ?",
|
||||
"element_not_found": "Élément avec l'id \"{id}\" non trouvé.",
|
||||
"search_placeholder": "Rechercher des fichiers ou un tag...",
|
||||
"file_name": "Nom du fichier",
|
||||
"date_modified": "Date de modification",
|
||||
"upload_date": "Date de téléchargement",
|
||||
"file_size": "Taille du fichier",
|
||||
"uploader": "Téléversé par",
|
||||
"enter_totp_code": "Entrez le code TOTP",
|
||||
"use_recovery_code_instead": "Utilisez le code de récupération à la place",
|
||||
"enter_recovery_code": "Entrez le code de récupération",
|
||||
"editing": "Modification",
|
||||
"decrease_font": "A-",
|
||||
"increase_font": "A+",
|
||||
"save": "Enregistrer",
|
||||
"close": "Fermer",
|
||||
"no_files_found": "Aucun fichier trouvé.",
|
||||
"switch_to_table_view": "Passer à la vue tableau",
|
||||
"switch_to_gallery_view": "Passer à la vue galerie",
|
||||
"share_file": "Partager le fichier",
|
||||
"set_expiration": "Définir l'expiration :",
|
||||
"password_optional": "Mot de passe (facultatif) :",
|
||||
"generate_share_link": "Générer un lien de partage",
|
||||
"shareable_link": "Lien partageable :",
|
||||
"copy_link": "Copier le lien",
|
||||
"tag_file": "Marquer le fichier",
|
||||
"tag_name": "Nom du tag :",
|
||||
"tag_color": "Couleur du tag :",
|
||||
"save_tag": "Enregistrer le tag",
|
||||
"files_in": "Fichiers dans",
|
||||
"light_mode": "Mode clair",
|
||||
"dark_mode": "Mode sombre",
|
||||
"upload_instruction": "Déposez vos fichiers/dossiers ici ou cliquez sur 'Choisir des fichiers'",
|
||||
"no_files_selected_default": "Aucun fichier sélectionné",
|
||||
"choose_files": "Choisir des fichiers",
|
||||
"delete_selected": "Supprimer la sélection",
|
||||
"copy_selected": "Copier la sélection",
|
||||
"move_selected": "Déplacer la sélection",
|
||||
"tag_selected": "Marquer la sélection",
|
||||
"download_zip": "Télécharger en Zip",
|
||||
"extract_zip": "Extraire le Zip",
|
||||
"preview": "Aperçu",
|
||||
"edit": "Modifier",
|
||||
"rename": "Renommer",
|
||||
"trash_empty": "La corbeille est vide.",
|
||||
"no_trash_selected": "Aucun élément de la corbeille sélectionné pour restauration.",
|
||||
|
||||
// Additional keys for HTML translations:
|
||||
"title": "FileRise",
|
||||
"header_title": "FileRise",
|
||||
"logout": "Se déconnecter",
|
||||
"change_password": "Changer le mot de passe",
|
||||
"restore_text": "Restaurer ou",
|
||||
"delete_text": "Supprimer les éléments de la corbeille",
|
||||
"restore_selected": "Restaurer la sélection",
|
||||
"restore_all": "Restaurer tout",
|
||||
"delete_selected_trash": "Supprimer la sélection",
|
||||
"delete_all": "Supprimer tout",
|
||||
"upload_header": "Téléverser des fichiers/dossiers",
|
||||
|
||||
// Folder Management keys:
|
||||
"folder_navigation": "Navigation et gestion des dossiers",
|
||||
"create_folder": "Créer un dossier",
|
||||
"create_folder_title": "Créer un dossier",
|
||||
"enter_folder_name": "Entrez le nom du dossier",
|
||||
"cancel": "Annuler",
|
||||
"create": "Créer",
|
||||
"rename_folder": "Renommer le dossier",
|
||||
"rename_folder_title": "Renommer le dossier",
|
||||
"rename_folder_placeholder": "Entrez le nouveau nom du dossier",
|
||||
"delete_folder": "Supprimer le dossier",
|
||||
"delete_folder_title": "Supprimer le dossier",
|
||||
"delete_folder_message": "Êtes-vous sûr de vouloir supprimer ce dossier ?",
|
||||
"folder_help": "Aide des dossiers",
|
||||
"folder_help_item_1": "Cliquez sur un dossier dans l'arborescence pour voir ses fichiers.",
|
||||
"folder_help_item_2": "Utilisez [-] pour réduire et [+] pour développer les dossiers.",
|
||||
"folder_help_item_3": "Sélectionnez un dossier et cliquez sur \"Créer un dossier\" pour ajouter un sous-dossier.",
|
||||
"folder_help_item_4": "Pour renommer ou supprimer un dossier, sélectionnez-le puis cliquez sur le bouton approprié.",
|
||||
|
||||
// File List keys:
|
||||
"file_list_title": "Fichiers dans (Racine)",
|
||||
"delete_files": "Supprimer les fichiers",
|
||||
"delete_selected_files_title": "Supprimer les fichiers sélectionnés",
|
||||
"delete_files_message": "Êtes-vous sûr de vouloir supprimer les fichiers sélectionnés ?",
|
||||
"copy_files": "Copier les fichiers",
|
||||
"copy_files_title": "Copier les fichiers sélectionnés",
|
||||
"copy_files_message": "Sélectionnez un dossier de destination pour copier les fichiers sélectionnés :",
|
||||
"move_files": "Déplacer les fichiers",
|
||||
"move_files_title": "Déplacer les fichiers sélectionnés",
|
||||
"move_files_message": "Sélectionnez un dossier de destination pour déplacer les fichiers sélectionnés :",
|
||||
"move": "Déplacer",
|
||||
"extract_zip_button": "Extraire le Zip",
|
||||
"download_zip_title": "Télécharger les fichiers sélectionnés en Zip",
|
||||
"download_zip_prompt": "Entrez un nom pour le fichier Zip :",
|
||||
"zip_placeholder": "fichiers.zip",
|
||||
|
||||
// Login Form keys:
|
||||
"login": "Connexion",
|
||||
"remember_me": "Se souvenir de moi",
|
||||
"login_oidc": "Connexion avec OIDC",
|
||||
"basic_http_login": "Utiliser l'authentification HTTP basique",
|
||||
|
||||
// Change Password keys:
|
||||
"change_password_title": "Changer le mot de passe",
|
||||
"old_password": "Ancien mot de passe",
|
||||
"new_password": "Nouveau mot de passe",
|
||||
"confirm_new_password": "Confirmer le nouveau mot de passe",
|
||||
|
||||
// Add User keys:
|
||||
"create_new_user_title": "Créer un nouvel utilisateur",
|
||||
"username": "Nom d'utilisateur :",
|
||||
"password": "Mot de passe :",
|
||||
"grant_admin": "Accorder les droits d'administrateur",
|
||||
"save_user": "Enregistrer l'utilisateur",
|
||||
|
||||
// Remove User keys:
|
||||
"remove_user_title": "Supprimer l'utilisateur",
|
||||
"select_user_remove": "Sélectionnez un utilisateur à supprimer :",
|
||||
"delete_user": "Supprimer l'utilisateur",
|
||||
|
||||
// Rename File keys:
|
||||
"rename_file_title": "Renommer le fichier",
|
||||
"rename_file_placeholder": "Entrez le nouveau nom du fichier",
|
||||
|
||||
// Custom Confirm Modal keys:
|
||||
"yes": "Oui",
|
||||
"no": "Non",
|
||||
"delete": "Supprimer",
|
||||
"download": "Télécharger",
|
||||
"upload": "Téléverser",
|
||||
"copy": "Copier",
|
||||
"extract": "Extraire",
|
||||
|
||||
// Dark Mode Toggle
|
||||
"dark_mode_toggle": "Mode sombre"
|
||||
},
|
||||
de: {
|
||||
"please_log_in_to_continue": "Bitte melden Sie sich an, um fortzufahren.",
|
||||
"no_files_selected": "Keine Dateien ausgewählt.",
|
||||
"confirm_delete_files": "Sind Sie sicher, dass Sie {count} ausgewählte Datei(en) löschen möchten?",
|
||||
"element_not_found": "Element mit der ID \"{id}\" wurde nicht gefunden.",
|
||||
"search_placeholder": "Suche nach Dateien oder Tags...",
|
||||
"file_name": "Dateiname",
|
||||
"date_modified": "Änderungsdatum",
|
||||
"upload_date": "Hochladedatum",
|
||||
"file_size": "Dateigröße",
|
||||
"uploader": "Hochgeladen von",
|
||||
"enter_totp_code": "Geben Sie den TOTP-Code ein",
|
||||
"use_recovery_code_instead": "Verwenden Sie stattdessen den Wiederherstellungscode",
|
||||
"enter_recovery_code": "Geben Sie den Wiederherstellungscode ein",
|
||||
"editing": "Bearbeitung",
|
||||
"decrease_font": "A-",
|
||||
"increase_font": "A+",
|
||||
"save": "Speichern",
|
||||
"close": "Schließen",
|
||||
"no_files_found": "Keine Dateien gefunden.",
|
||||
"switch_to_table_view": "Zur Tabellenansicht wechseln",
|
||||
"switch_to_gallery_view": "Zur Galerieansicht wechseln",
|
||||
"share_file": "Datei teilen",
|
||||
"set_expiration": "Ablauf festlegen:",
|
||||
"password_optional": "Passwort (optional):",
|
||||
"generate_share_link": "Freigabelink generieren",
|
||||
"shareable_link": "Freigabelink:",
|
||||
"copy_link": "Link kopieren",
|
||||
"tag_file": "Datei taggen",
|
||||
"tag_name": "Tagname:",
|
||||
"tag_color": "Tagfarbe:",
|
||||
"save_tag": "Tag speichern",
|
||||
"files_in": "Dateien in",
|
||||
"light_mode": "Heller Modus",
|
||||
"dark_mode": "Dunkler Modus",
|
||||
"upload_instruction": "Ziehen Sie Dateien/Ordner hierher oder klicken Sie auf 'Dateien auswählen'",
|
||||
"no_files_selected_default": "Keine Dateien ausgewählt",
|
||||
"choose_files": "Dateien auswählen",
|
||||
"delete_selected": "Ausgewählte löschen",
|
||||
"copy_selected": "Ausgewählte kopieren",
|
||||
"move_selected": "Ausgewählte verschieben",
|
||||
"tag_selected": "Ausgewählte taggen",
|
||||
"download_zip": "Zip herunterladen",
|
||||
"extract_zip": "Zip entpacken",
|
||||
"preview": "Vorschau",
|
||||
"edit": "Bearbeiten",
|
||||
"rename": "Umbenennen",
|
||||
"trash_empty": "Papierkorb ist leer.",
|
||||
"no_trash_selected": "Keine Elemente im Papierkorb für die Wiederherstellung ausgewählt.",
|
||||
|
||||
// Additional keys for HTML translations:
|
||||
"title": "FileRise",
|
||||
"header_title": "FileRise",
|
||||
"logout": "Abmelden",
|
||||
"change_password": "Passwort ändern",
|
||||
"restore_text": "Wiederherstellen oder",
|
||||
"delete_text": "Papierkorbeinträge löschen",
|
||||
"restore_selected": "Ausgewählte wiederherstellen",
|
||||
"restore_all": "Alle wiederherstellen",
|
||||
"delete_selected_trash": "Ausgewählte löschen",
|
||||
"delete_all": "Alle löschen",
|
||||
"upload_header": "Dateien/Ordner hochladen",
|
||||
|
||||
// Folder Management keys:
|
||||
"folder_navigation": "Ordnernavigation & Verwaltung",
|
||||
"create_folder": "Ordner erstellen",
|
||||
"create_folder_title": "Ordner erstellen",
|
||||
"enter_folder_name": "Geben Sie den Ordnernamen ein",
|
||||
"cancel": "Abbrechen",
|
||||
"create": "Erstellen",
|
||||
"rename_folder": "Ordner umbenennen",
|
||||
"rename_folder_title": "Ordner umbenennen",
|
||||
"rename_folder_placeholder": "Neuen Ordnernamen eingeben",
|
||||
"delete_folder": "Ordner löschen",
|
||||
"delete_folder_title": "Ordner löschen",
|
||||
"delete_folder_message": "Sind Sie sicher, dass Sie diesen Ordner löschen möchten?",
|
||||
"folder_help": "Ordnerhilfe",
|
||||
"folder_help_item_1": "Klicken Sie auf einen Ordner, um dessen Dateien anzuzeigen.",
|
||||
"folder_help_item_2": "Verwenden Sie [-] um zu minimieren und [+] um zu erweitern.",
|
||||
"folder_help_item_3": "Klicken Sie auf \"Ordner erstellen\", um einen Unterordner hinzuzufügen.",
|
||||
"folder_help_item_4": "Um einen Ordner umzubenennen oder zu löschen, wählen Sie ihn und klicken Sie auf die entsprechende Schaltfläche.",
|
||||
|
||||
// File List keys:
|
||||
"file_list_title": "Dateien in (Root)",
|
||||
"delete_files": "Dateien löschen",
|
||||
"delete_selected_files_title": "Ausgewählte Dateien löschen",
|
||||
"delete_files_message": "Sind Sie sicher, dass Sie die ausgewählten Dateien löschen möchten?",
|
||||
"copy_files": "Dateien kopieren",
|
||||
"copy_files_title": "Ausgewählte Dateien kopieren",
|
||||
"copy_files_message": "Wählen Sie einen Zielordner, um die ausgewählten Dateien zu kopieren:",
|
||||
"move_files": "Dateien verschieben",
|
||||
"move_files_title": "Ausgewählte Dateien verschieben",
|
||||
"move_files_message": "Wählen Sie einen Zielordner, um die ausgewählten Dateien zu verschieben:",
|
||||
"move": "Verschieben",
|
||||
"extract_zip_button": "Zip entpacken",
|
||||
"download_zip_title": "Ausgewählte Dateien als Zip herunterladen",
|
||||
"download_zip_prompt": "Geben Sie einen Namen für die Zip-Datei ein:",
|
||||
"zip_placeholder": "dateien.zip",
|
||||
|
||||
// Login Form keys:
|
||||
"login": "Anmelden",
|
||||
"remember_me": "Angemeldet bleiben",
|
||||
"login_oidc": "Mit OIDC anmelden",
|
||||
"basic_http_login": "HTTP-Basisauthentifizierung verwenden",
|
||||
|
||||
// Change Password keys:
|
||||
"change_password_title": "Passwort ändern",
|
||||
"old_password": "Altes Passwort",
|
||||
"new_password": "Neues Passwort",
|
||||
"confirm_new_password": "Neues Passwort bestätigen",
|
||||
|
||||
// Add User keys:
|
||||
"create_new_user_title": "Neuen Benutzer erstellen",
|
||||
"username": "Benutzername:",
|
||||
"password": "Passwort:",
|
||||
"grant_admin": "Admin-Rechte vergeben",
|
||||
"save_user": "Benutzer speichern",
|
||||
|
||||
// Remove User keys:
|
||||
"remove_user_title": "Benutzer entfernen",
|
||||
"select_user_remove": "Wählen Sie einen Benutzer zum Entfernen:",
|
||||
"delete_user": "Benutzer löschen",
|
||||
|
||||
// Rename File keys:
|
||||
"rename_file_title": "Datei umbenennen",
|
||||
"rename_file_placeholder": "Neuen Dateinamen eingeben",
|
||||
|
||||
// Custom Confirm Modal keys:
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"delete": "Löschen",
|
||||
"download": "Herunterladen",
|
||||
"upload": "Hochladen",
|
||||
"copy": "Kopieren",
|
||||
"extract": "Entpacken",
|
||||
|
||||
// Dark Mode Toggle
|
||||
"dark_mode_toggle": "Dunkler Modus"
|
||||
}
|
||||
};
|
||||
|
||||
let currentLocale = 'en';
|
||||
|
||||
export function setLocale(locale) {
|
||||
currentLocale = locale;
|
||||
}
|
||||
|
||||
export function t(key, placeholders) {
|
||||
const localeTranslations = translations[currentLocale] || {};
|
||||
let translation = localeTranslations[key] || key;
|
||||
if (placeholders) {
|
||||
Object.keys(placeholders).forEach(ph => {
|
||||
translation = translation.replace(`{${ph}}`, placeholders[ph]);
|
||||
});
|
||||
}
|
||||
return translation;
|
||||
}
|
||||
|
||||
export function applyTranslations() {
|
||||
document.querySelectorAll('[data-i18n-key]').forEach(el => {
|
||||
el.innerText = t(el.getAttribute('data-i18n-key'));
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||
el.setAttribute('placeholder', t(el.getAttribute('data-i18n-placeholder')));
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||
el.setAttribute('title', t(el.getAttribute('data-i18n-title')));
|
||||
});
|
||||
}
|
||||
192
js/main.js
192
js/main.js
@@ -1,192 +0,0 @@
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import { initUpload } from './upload.js';
|
||||
import { initAuth, checkAuthentication } from './auth.js';
|
||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
||||
import { displayFilePreview } from './filePreview.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload } from './fileActions.js';
|
||||
import { editFile, saveFile } from './fileEditor.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
|
||||
function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
|
||||
return fetch('token.php', { credentials: 'include' })
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Token fetch failed with status: " + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Set global variables.
|
||||
window.csrfToken = data.csrf_token;
|
||||
window.SHARE_URL = data.share_url;
|
||||
|
||||
// Update (or create) the CSRF meta tag.
|
||||
let metaCSRF = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!metaCSRF) {
|
||||
metaCSRF = document.createElement('meta');
|
||||
metaCSRF.name = 'csrf-token';
|
||||
document.head.appendChild(metaCSRF);
|
||||
}
|
||||
metaCSRF.setAttribute('content', data.csrf_token);
|
||||
|
||||
// Update (or create) the share URL meta tag.
|
||||
let metaShare = document.querySelector('meta[name="share-url"]');
|
||||
if (!metaShare) {
|
||||
metaShare = document.createElement('meta');
|
||||
metaShare.name = 'share-url';
|
||||
document.head.appendChild(metaShare);
|
||||
}
|
||||
metaShare.setAttribute('content', data.share_url);
|
||||
|
||||
return data;
|
||||
})
|
||||
.catch(error => {
|
||||
if (retries > 0) {
|
||||
console.warn(`CSRF token load failed. Retrying in ${delay}ms... (${retries} retries left)`, error);
|
||||
return new Promise(resolve => setTimeout(resolve, delay))
|
||||
.then(() => loadCsrfTokenWithRetry(retries - 1, delay * 2));
|
||||
}
|
||||
console.error("Failed to load CSRF token after retries.", error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Expose functions for inline handlers.
|
||||
window.sendRequest = sendRequest;
|
||||
window.toggleVisibility = toggleVisibility;
|
||||
window.toggleAllCheckboxes = toggleAllCheckboxes;
|
||||
window.editFile = editFile;
|
||||
window.saveFile = saveFile;
|
||||
window.renameFile = renameFile;
|
||||
window.confirmSingleDownload = confirmSingleDownload;
|
||||
window.openDownloadModal = openDownloadModal;
|
||||
|
||||
// Global variable for the current folder.
|
||||
window.currentFolder = "root";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Retrieve the saved language from localStorage; default to "en"
|
||||
const savedLanguage = localStorage.getItem("language") || "en";
|
||||
// Set the locale based on the saved language
|
||||
setLocale(savedLanguage);
|
||||
// Apply the translations to update the UI
|
||||
applyTranslations();
|
||||
// First, load the CSRF token (with retry).
|
||||
loadCsrfTokenWithRetry().then(() => {
|
||||
// Once CSRF token is loaded, initialize authentication.
|
||||
initAuth();
|
||||
|
||||
// Continue with initializations that rely on a valid CSRF token:
|
||||
checkAuthentication().then(authenticated => {
|
||||
if (authenticated) {
|
||||
window.currentFolder = "root";
|
||||
initTagSearch();
|
||||
loadFileList(window.currentFolder);
|
||||
initDragAndDrop();
|
||||
loadSidebarOrder();
|
||||
loadHeaderOrder();
|
||||
initFileActions();
|
||||
initUpload();
|
||||
loadFolderTree();
|
||||
setupTrashRestoreDelete();
|
||||
|
||||
const helpBtn = document.getElementById("folderHelpBtn");
|
||||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||||
helpBtn.addEventListener("click", function () {
|
||||
// Toggle display of the tooltip.
|
||||
if (helpTooltip.style.display === "none" || helpTooltip.style.display === "") {
|
||||
helpTooltip.style.display = "block";
|
||||
} else {
|
||||
helpTooltip.style.display = "none";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn("User not authenticated. Data loading deferred.");
|
||||
}
|
||||
});
|
||||
|
||||
// Other DOM initialization that can happen after CSRF is ready.
|
||||
const newPasswordInput = document.getElementById("newPassword");
|
||||
if (newPasswordInput) {
|
||||
newPasswordInput.addEventListener("input", function () {
|
||||
console.log("newPassword input event:", this.value);
|
||||
});
|
||||
} else {
|
||||
console.error("newPassword input not found!");
|
||||
}
|
||||
|
||||
// --- Dark Mode Persistence ---
|
||||
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||
const storedDarkMode = localStorage.getItem("darkMode");
|
||||
|
||||
if (storedDarkMode === "true") {
|
||||
document.body.classList.add("dark-mode");
|
||||
} else if (storedDarkMode === "false") {
|
||||
document.body.classList.remove("dark-mode");
|
||||
} else {
|
||||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
document.body.classList.add("dark-mode");
|
||||
} else {
|
||||
document.body.classList.remove("dark-mode");
|
||||
}
|
||||
}
|
||||
|
||||
if (darkModeToggle) {
|
||||
darkModeToggle.textContent = document.body.classList.contains("dark-mode")
|
||||
? "Light Mode"
|
||||
: "Dark Mode";
|
||||
|
||||
darkModeToggle.addEventListener("click", function () {
|
||||
if (document.body.classList.contains("dark-mode")) {
|
||||
document.body.classList.remove("dark-mode");
|
||||
localStorage.setItem("darkMode", "false");
|
||||
darkModeToggle.textContent = "Dark Mode";
|
||||
} else {
|
||||
document.body.classList.add("dark-mode");
|
||||
localStorage.setItem("darkMode", "true");
|
||||
darkModeToggle.textContent = "Light Mode";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (localStorage.getItem("darkMode") === null && window.matchMedia) {
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (event) => {
|
||||
if (event.matches) {
|
||||
document.body.classList.add("dark-mode");
|
||||
if (darkModeToggle) darkModeToggle.textContent = t("light_mode");
|
||||
} else {
|
||||
document.body.classList.remove("dark-mode");
|
||||
if (darkModeToggle) darkModeToggle.textContent = t("dark_mode");
|
||||
}
|
||||
});
|
||||
}
|
||||
// --- End Dark Mode Persistence ---
|
||||
|
||||
const message = sessionStorage.getItem("welcomeMessage");
|
||||
if (message) {
|
||||
showToast(message);
|
||||
sessionStorage.removeItem("welcomeMessage");
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Initialization halted due to CSRF token load failure.", error);
|
||||
});
|
||||
|
||||
// --- Auto-scroll During Drag ---
|
||||
// Adjust these values as needed:
|
||||
const SCROLL_THRESHOLD = 50; // pixels from edge to start scrolling
|
||||
const SCROLL_SPEED = 20; // pixels to scroll per event
|
||||
|
||||
document.addEventListener("dragover", function (e) {
|
||||
if (e.clientY < SCROLL_THRESHOLD) {
|
||||
window.scrollBy(0, -SCROLL_SPEED);
|
||||
} else if (e.clientY > window.innerHeight - SCROLL_THRESHOLD) {
|
||||
window.scrollBy(0, SCROLL_SPEED);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
export function sendRequest(url, method = "GET", data = null, customHeaders = {}) {
|
||||
const options = {
|
||||
method,
|
||||
credentials: 'include',
|
||||
headers: {}
|
||||
};
|
||||
|
||||
// Merge custom headers
|
||||
Object.assign(options.headers, customHeaders);
|
||||
|
||||
// If data is provided and is not FormData, assume JSON.
|
||||
if (data && !(data instanceof FormData)) {
|
||||
if (!options.headers["Content-Type"]) {
|
||||
options.headers["Content-Type"] = "application/json";
|
||||
}
|
||||
options.body = JSON.stringify(data);
|
||||
} else if (data instanceof FormData) {
|
||||
options.body = data;
|
||||
}
|
||||
|
||||
return fetch(url, options)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(`HTTP error ${response.status}: ${text}`);
|
||||
});
|
||||
}
|
||||
const clonedResponse = response.clone();
|
||||
return response.json().catch(() => clonedResponse.text());
|
||||
});
|
||||
}
|
||||
120
login_basic.php
120
login_basic.php
@@ -1,120 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE; // Make sure the users file path is defined
|
||||
|
||||
// Helper: retrieve a user's TOTP secret from users.txt
|
||||
function getUserTOTPSecret($username) {
|
||||
global $encryptionKey, $usersFile;
|
||||
if (!file_exists($usersFile)) return null;
|
||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
$parts = explode(':', trim($line));
|
||||
if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
|
||||
return decryptData($parts[3], $encryptionKey);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reuse the same authentication function
|
||||
function authenticate($username, $password)
|
||||
{
|
||||
global $usersFile;
|
||||
if (!file_exists($usersFile)) {
|
||||
error_log("authenticate(): users file not found");
|
||||
return false;
|
||||
}
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3);
|
||||
if ($username === $storedUser && password_verify($password, $storedPass)) {
|
||||
return $storedRole; // Return the user's role
|
||||
}
|
||||
}
|
||||
error_log("authenticate(): authentication failed for '$username'");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Define helper function to get a user's role from users.txt
|
||||
function getUserRole($username) {
|
||||
global $usersFile;
|
||||
if (file_exists($usersFile)) {
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(":", trim($line));
|
||||
if (count($parts) >= 3 && $parts[0] === $username) {
|
||||
return trim($parts[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add the loadFolderPermission function here:
|
||||
function loadFolderPermission($username) {
|
||||
global $encryptionKey;
|
||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||
if (file_exists($permissionsFile)) {
|
||||
$content = file_get_contents($permissionsFile);
|
||||
$decrypted = decryptData($content, $encryptionKey);
|
||||
$permissions = $decrypted !== false ? json_decode($decrypted, true) : json_decode($content, true);
|
||||
if (is_array($permissions)) {
|
||||
foreach ($permissions as $storedUsername => $data) {
|
||||
if (strcasecmp($storedUsername, $username) === 0 && isset($data['folderOnly'])) {
|
||||
return (bool)$data['folderOnly'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the user has sent HTTP Basic auth credentials.
|
||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
header('WWW-Authenticate: Basic realm="FileRise Login"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo 'Authorization Required';
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = trim($_SERVER['PHP_AUTH_USER']);
|
||||
$password = trim($_SERVER['PHP_AUTH_PW']);
|
||||
|
||||
// Validate username format (optional)
|
||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
||||
header('WWW-Authenticate: Basic realm="FileRise Login"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo 'Invalid username format';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Attempt authentication
|
||||
$roleFromAuth = authenticate($username, $password);
|
||||
if ($roleFromAuth !== false) {
|
||||
// --- NEW: check for TOTP secret ---
|
||||
$secret = getUserTOTPSecret($username);
|
||||
if ($secret) {
|
||||
// hold user & secret in session and ask client for TOTP
|
||||
$_SESSION['pending_login_user'] = $username;
|
||||
$_SESSION['pending_login_secret'] = $secret;
|
||||
header("Location: index.html?totp_required=1");
|
||||
exit;
|
||||
}
|
||||
|
||||
// no TOTP, proceed as before
|
||||
session_regenerate_id(true);
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $username;
|
||||
$_SESSION["isAdmin"] = (getUserRole($username) === "1");
|
||||
$_SESSION["folderOnly"] = loadFolderPermission($username);
|
||||
|
||||
header("Location: index.html");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Invalid credentials; prompt again
|
||||
header('WWW-Authenticate: Basic realm="FileRise Login"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo 'Invalid credentials';
|
||||
exit;
|
||||
?>
|
||||
50
logout.php
50
logout.php
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
// Retrieve headers and check CSRF token.
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
// Log CSRF mismatch but proceed with logout.
|
||||
if (isset($_SESSION['csrf_token']) && $receivedToken !== $_SESSION['csrf_token']) {
|
||||
error_log("CSRF token mismatch on logout. Proceeding with logout.");
|
||||
}
|
||||
|
||||
// Remove the remember_me token.
|
||||
if (isset($_COOKIE['remember_me_token'])) {
|
||||
$token = $_COOKIE['remember_me_token'];
|
||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
||||
if (file_exists($persistentTokensFile)) {
|
||||
$encryptedContent = file_get_contents($persistentTokensFile);
|
||||
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
||||
$persistentTokens = json_decode($decryptedContent, true);
|
||||
if (is_array($persistentTokens) && isset($persistentTokens[$token])) {
|
||||
unset($persistentTokens[$token]);
|
||||
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||
file_put_contents($persistentTokensFile, $newEncryptedContent, LOCK_EX);
|
||||
}
|
||||
}
|
||||
// Clear the cookie.
|
||||
// Ensure $secure is defined; for example:
|
||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
|
||||
}
|
||||
|
||||
// Clear session data and remove session cookie.
|
||||
$_SESSION = [];
|
||||
|
||||
// Clear the session cookie.
|
||||
if (ini_get("session.use_cookies")) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000,
|
||||
$params["path"], $params["domain"],
|
||||
$params["secure"], $params["httponly"]
|
||||
);
|
||||
}
|
||||
|
||||
// Destroy the session.
|
||||
session_destroy();
|
||||
|
||||
header("Location: index.html?logout=1");
|
||||
exit;
|
||||
?>
|
||||
167
moveFiles.php
167
moveFiles.php
@@ -1,167 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to move files."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (
|
||||
!$data ||
|
||||
!isset($data['source']) ||
|
||||
!isset($data['destination']) ||
|
||||
!isset($data['files'])
|
||||
) {
|
||||
echo json_encode(["error" => "Invalid request"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sourceFolder = trim($data['source']) ?: 'root';
|
||||
$destinationFolder = trim($data['destination']) ?: 'root';
|
||||
|
||||
// Allow only letters, numbers, underscores, dashes, spaces, and forward slashes in folder names.
|
||||
$folderPattern = '/^[A-Za-z0-9_\- \/]+$/';
|
||||
if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) {
|
||||
echo json_encode(["error" => "Invalid source folder name."]);
|
||||
exit;
|
||||
}
|
||||
if ($destinationFolder !== 'root' && !preg_match($folderPattern, $destinationFolder)) {
|
||||
echo json_encode(["error" => "Invalid destination folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Remove any leading/trailing slashes.
|
||||
$sourceFolder = trim($sourceFolder, "/\\ ");
|
||||
$destinationFolder = trim($destinationFolder, "/\\ ");
|
||||
|
||||
// Build the source and destination directories.
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$sourceDir = ($sourceFolder === 'root')
|
||||
? $baseDir . DIRECTORY_SEPARATOR
|
||||
: $baseDir . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR;
|
||||
$destDir = ($destinationFolder === 'root')
|
||||
? $baseDir . DIRECTORY_SEPARATOR
|
||||
: $baseDir . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR;
|
||||
|
||||
// Ensure destination directory exists.
|
||||
if (!is_dir($destDir)) {
|
||||
if (!mkdir($destDir, 0775, true)) {
|
||||
echo json_encode(["error" => "Could not create destination folder"]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Generate the metadata file path for a given folder.
|
||||
function getMetadataFilePath($folder) {
|
||||
if (strtolower($folder) === 'root' || $folder === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
|
||||
}
|
||||
|
||||
// Helper: Generate a unique file name if a file with the same name exists.
|
||||
function getUniqueFileName($destDir, $fileName) {
|
||||
$fullPath = $destDir . $fileName;
|
||||
clearstatcache(true, $fullPath);
|
||||
if (!file_exists($fullPath)) {
|
||||
return $fileName;
|
||||
}
|
||||
$basename = pathinfo($fileName, PATHINFO_FILENAME);
|
||||
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
|
||||
$counter = 1;
|
||||
do {
|
||||
$newName = $basename . " (" . $counter . ")" . ($extension ? "." . $extension : "");
|
||||
$newFullPath = $destDir . $newName;
|
||||
clearstatcache(true, $newFullPath);
|
||||
$counter++;
|
||||
} while (file_exists($destDir . $newName));
|
||||
return $newName;
|
||||
}
|
||||
|
||||
// Prepare metadata files.
|
||||
$srcMetaFile = getMetadataFilePath($sourceFolder);
|
||||
$destMetaFile = getMetadataFilePath($destinationFolder);
|
||||
|
||||
$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : [];
|
||||
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
|
||||
|
||||
$errors = [];
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
|
||||
foreach ($data['files'] as $fileName) {
|
||||
// Save the original name for metadata lookup.
|
||||
$originalName = basename(trim($fileName));
|
||||
$basename = $originalName; // Start with the original name.
|
||||
|
||||
// Validate the file name.
|
||||
if (!preg_match($safeFileNamePattern, $basename)) {
|
||||
$errors[] = "$basename has invalid characters.";
|
||||
continue;
|
||||
}
|
||||
|
||||
$srcPath = $sourceDir . $originalName;
|
||||
$destPath = $destDir . $basename;
|
||||
|
||||
clearstatcache();
|
||||
if (!file_exists($srcPath)) {
|
||||
$errors[] = "$originalName does not exist in source.";
|
||||
continue;
|
||||
}
|
||||
|
||||
// If a file with the same name exists in destination, generate a unique name.
|
||||
if (file_exists($destPath)) {
|
||||
$uniqueName = getUniqueFileName($destDir, $basename);
|
||||
$basename = $uniqueName;
|
||||
$destPath = $destDir . $uniqueName;
|
||||
}
|
||||
|
||||
if (!rename($srcPath, $destPath)) {
|
||||
$errors[] = "Failed to move $basename";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update metadata: if there is metadata for the original file, move it under the new name.
|
||||
if (isset($srcMetadata[$originalName])) {
|
||||
$destMetadata[$basename] = $srcMetadata[$originalName];
|
||||
unset($srcMetadata[$originalName]);
|
||||
}
|
||||
}
|
||||
|
||||
if (file_put_contents($srcMetaFile, json_encode($srcMetadata, JSON_PRETTY_PRINT)) === false) {
|
||||
$errors[] = "Failed to update source metadata.";
|
||||
}
|
||||
if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) {
|
||||
$errors[] = "Failed to update destination metadata.";
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
echo json_encode(["success" => "Files moved successfully"]);
|
||||
} else {
|
||||
echo json_encode(["error" => implode("; ", $errors)]);
|
||||
}
|
||||
?>
|
||||
2599
openapi.json.dist
Normal file
2599
openapi.json.dist
Normal file
File diff suppressed because it is too large
Load Diff
31
public/api.php
Normal file
31
public/api.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
// public/api.php
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
|
||||
if (empty($_SESSION['authenticated'])) {
|
||||
header('Location: /index.html?redirect=/api.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['spec'])) {
|
||||
header('Content-Type: application/json');
|
||||
readfile(__DIR__ . '/../openapi.json.dist');
|
||||
exit;
|
||||
}
|
||||
|
||||
?><!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>FileRise API Docs</title>
|
||||
<script defer src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"
|
||||
integrity="sha384-4vOjrBu7SuDWXcAw1qFznVLA/sKL+0l4nn+J1HY8w7cpa6twQEYuh4b0Cwuo7CyX"
|
||||
crossorigin="anonymous"></script>
|
||||
<script defer src="/js/redoc-init.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="api.php?spec=1"></redoc>
|
||||
<div id="redoc-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
8
public/api/addUser.php
Normal file
8
public/api/addUser.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/addUser.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->addUser();
|
||||
8
public/api/admin/getConfig.php
Normal file
8
public/api/admin/getConfig.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/admin/getConfig.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||
|
||||
$adminController = new AdminController();
|
||||
$adminController->getConfig();
|
||||
63
public/api/admin/readMetadata.php
Normal file
63
public/api/admin/readMetadata.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
// public/api/admin/readMetadata.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
|
||||
// Only admins may read these
|
||||
if (empty($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Must supply ?file=share_links.json or share_folder_links.json
|
||||
if (empty($_GET['file'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing `file` parameter']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = basename($_GET['file']);
|
||||
$allowed = ['share_links.json', 'share_folder_links.json'];
|
||||
if (!in_array($file, $allowed, true)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Invalid file requested']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = META_DIR . $file;
|
||||
if (!file_exists($path)) {
|
||||
// Return empty object so JS sees `{}` not an error
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode((object)[]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$jsonData = file_get_contents($path);
|
||||
$data = json_decode($jsonData, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Corrupted JSON']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ——— Clean up expired entries ———
|
||||
$now = time();
|
||||
$changed = false;
|
||||
foreach ($data as $token => $entry) {
|
||||
if (!empty($entry['expires']) && $entry['expires'] < $now) {
|
||||
unset($data[$token]);
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
// overwrite file with cleaned data
|
||||
file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
// ——— Send cleaned data back ———
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
8
public/api/admin/updateConfig.php
Normal file
8
public/api/admin/updateConfig.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/admin/updateConfig.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||
|
||||
$adminController = new AdminController();
|
||||
$adminController->updateConfig();
|
||||
9
public/api/auth/auth.php
Normal file
9
public/api/auth/auth.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// public/api/auth/auth.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->auth();
|
||||
8
public/api/auth/checkAuth.php
Normal file
8
public/api/auth/checkAuth.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/auth/checkAuth.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->checkAuth();
|
||||
8
public/api/auth/login_basic.php
Normal file
8
public/api/auth/login_basic.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/auth/login_basic.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->loginBasic();
|
||||
8
public/api/auth/logout.php
Normal file
8
public/api/auth/logout.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/auth/logout.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->logout();
|
||||
8
public/api/auth/token.php
Normal file
8
public/api/auth/token.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/auth/token.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->getToken();
|
||||
8
public/api/changePassword.php
Normal file
8
public/api/changePassword.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/changePassword.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->changePassword();
|
||||
8
public/api/file/copyFiles.php
Normal file
8
public/api/file/copyFiles.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/copyFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->copyFiles();
|
||||
15
public/api/file/createFile.php
Normal file
15
public/api/file/createFile.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
// public/api/file/createFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
if (empty($_SESSION['authenticated'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success'=>false,'error'=>'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$fc = new FileController();
|
||||
$fc->createFile();
|
||||
8
public/api/file/createShareLink.php
Normal file
8
public/api/file/createShareLink.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/createShareLink.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->createShareLink();
|
||||
8
public/api/file/deleteFiles.php
Normal file
8
public/api/file/deleteFiles.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/deleteFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.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();
|
||||
8
public/api/file/deleteTrashFiles.php
Normal file
8
public/api/file/deleteTrashFiles.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/deleteTrashFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->deleteTrashFiles();
|
||||
8
public/api/file/download.php
Normal file
8
public/api/file/download.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/download.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->downloadFile();
|
||||
8
public/api/file/downloadZip.php
Normal file
8
public/api/file/downloadZip.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/downloadZip.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->downloadZip();
|
||||
8
public/api/file/extractZip.php
Normal file
8
public/api/file/extractZip.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/extractZip.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->extractZip();
|
||||
8
public/api/file/getFileList.php
Normal file
8
public/api/file/getFileList.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/getFileList.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getFileList();
|
||||
8
public/api/file/getFileTag.php
Normal file
8
public/api/file/getFileTag.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/getFileTag.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.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();
|
||||
8
public/api/file/getTrashItems.php
Normal file
8
public/api/file/getTrashItems.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/getTrashItems.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getTrashItems();
|
||||
8
public/api/file/moveFiles.php
Normal file
8
public/api/file/moveFiles.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/moveFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->moveFiles();
|
||||
8
public/api/file/renameFile.php
Normal file
8
public/api/file/renameFile.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/renameFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->renameFile();
|
||||
8
public/api/file/restoreFiles.php
Normal file
8
public/api/file/restoreFiles.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/restoreFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->restoreFiles();
|
||||
8
public/api/file/saveFile.php
Normal file
8
public/api/file/saveFile.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/saveFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->saveFile();
|
||||
8
public/api/file/saveFileTag.php
Normal file
8
public/api/file/saveFileTag.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/saveFileTag.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->saveFileTag();
|
||||
8
public/api/file/share.php
Normal file
8
public/api/file/share.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/share.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->shareFile();
|
||||
8
public/api/folder/createFolder.php
Normal file
8
public/api/folder/createFolder.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/createFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->createFolder();
|
||||
8
public/api/folder/createShareFolderLink.php
Normal file
8
public/api/folder/createShareFolderLink.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/createShareFolderLink.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->createShareFolderLink();
|
||||
8
public/api/folder/deleteFolder.php
Normal file
8
public/api/folder/deleteFolder.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/deleteFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.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();
|
||||
8
public/api/folder/downloadSharedFile.php
Normal file
8
public/api/folder/downloadSharedFile.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/downloadSharedFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->downloadSharedFile();
|
||||
8
public/api/folder/getFolderList.php
Normal file
8
public/api/folder/getFolderList.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/getFolderList.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.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();
|
||||
8
public/api/folder/renameFolder.php
Normal file
8
public/api/folder/renameFolder.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/renameFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->renameFolder();
|
||||
8
public/api/folder/shareFolder.php
Normal file
8
public/api/folder/shareFolder.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/shareFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->shareFolder();
|
||||
8
public/api/folder/uploadToSharedFolder.php
Normal file
8
public/api/folder/uploadToSharedFolder.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/uploadToSharedFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->uploadToSharedFolder();
|
||||
8
public/api/getUserPermissions.php
Normal file
8
public/api/getUserPermissions.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/getUserPermissions.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->getUserPermissions();
|
||||
8
public/api/getUsers.php
Normal file
8
public/api/getUsers.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/getUsers.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->getUsers(); // This will output the JSON response
|
||||
15
public/api/profile/getCurrentUser.php
Normal file
15
public/api/profile/getCurrentUser.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($_SESSION['authenticated'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error'=>'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = $_SESSION['username'];
|
||||
$data = UserModel::getUser($user);
|
||||
echo json_encode($data);
|
||||
17
public/api/profile/uploadPicture.php
Normal file
17
public/api/profile/uploadPicture.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
// Always JSON, even on PHP notices
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$userController = new UserController();
|
||||
$userController->uploadPicture();
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Exception: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
8
public/api/removeUser.php
Normal file
8
public/api/removeUser.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/removeUser.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->removeUser();
|
||||
9
public/api/totp_disable.php
Normal file
9
public/api/totp_disable.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// public/api/totp_disable.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->disableTOTP();
|
||||
8
public/api/totp_recover.php
Normal file
8
public/api/totp_recover.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/totp_recover.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->recoverTOTP();
|
||||
8
public/api/totp_saveCode.php
Normal file
8
public/api/totp_saveCode.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/totp_saveCode.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->saveTOTPRecoveryCode();
|
||||
9
public/api/totp_setup.php
Normal file
9
public/api/totp_setup.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// public/api/totp_setup.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->setupTOTP();
|
||||
9
public/api/totp_verify.php
Normal file
9
public/api/totp_verify.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// public/api/totp_verify.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->verifyTOTP();
|
||||
8
public/api/updateUserPanel.php
Normal file
8
public/api/updateUserPanel.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/updateUserPanel.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->updateUserPanel();
|
||||
8
public/api/updateUserPermissions.php
Normal file
8
public/api/updateUserPermissions.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/updateUserPermissions.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->updateUserPermissions();
|
||||
8
public/api/upload/removeChunks.php
Normal file
8
public/api/upload/removeChunks.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/upload/removeChunks.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UploadController.php';
|
||||
|
||||
$uploadController = new UploadController();
|
||||
$uploadController->removeChunks();
|
||||
7
public/api/upload/upload.php
Normal file
7
public/api/upload/upload.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
// public/api/upload/upload.php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UploadController.php';
|
||||
|
||||
$uploadController = new UploadController();
|
||||
$uploadController->handleUpload();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user