Compare commits

...

198 Commits

Author SHA1 Message Date
Ryan
8c70783d5a fix(upload): relax filename validation regex to allow broader Unicode and special chars (closes #29) 2025-05-08 04:58:57 -04:00
Ryan
b4d6f01432 feat(admin): add proxy-only auth bypass and configurable auth header (closes #28) 2025-05-08 04:43:33 -04:00
Ryan
d48b15a5f4 screenshots updated 2025-05-05 08:49:27 -04:00
Ryan
d1726f0160 Refactor auth flow: add loading overlay, separate login, extract initializeApp 2025-05-05 07:28:28 -04:00
Ryan
bd1841b788 Unify modals: shared close button, truncate long filenames, fix sizing & overflow 2025-05-04 15:44:43 -04:00
Ryan
bde35d1d31 Extend clean up expired shared entries 2025-05-04 02:28:33 -04:00
Ryan
8d6a1be777 Fix FolderController readOnly create folder permission 2025-05-04 01:22:43 -04:00
Ryan
56f34ba362 Admin Panel Refactor & Enhancements 2025-05-04 00:23:46 -04:00
Ryan
4d329e046f Remove old controllers 2025-05-04 00:02:38 -04:00
Ryan
f3977153fb Refactor AdminPanel: extract module, add collapsible sections & shared-links management, enforce PascalCase controllers 2025-05-03 23:41:02 -04:00
Ryan
274bedd186 Improve PDF preview and input focus behaviors 2025-04-30 00:53:44 -04:00
Ryan
2e4dbe7f7f support custom expiration durations for file and folder shares (closes #26) 2025-04-28 20:02:11 -04:00
Ryan
0334e443eb fix(shared-folder): sanitize gallery rendering to avoid innerHTML and resolve CodeQL warning (fixes #27) 2025-04-27 18:28:39 -04:00
Ryan
76f5ed5c96 shared-folder: externalize gallery view JS & enforce CSP compliance 2025-04-27 18:18:00 -04:00
Ryan
18f588dc24 Update Manual Installation 2025-04-27 17:23:43 -04:00
Ryan
491c686762 fix: advancedSearchToggle in renderFileTable & renderGalleryView 2025-04-27 17:08:49 -04:00
Ryan
25303df677 fixed: Pagination controls & Items-per-page dropdown 2025-04-27 16:50:22 -04:00
Ryan
ae0d63b86f fix: checkbox in toolbar does not select all files (Fixes #25) 2025-04-27 15:34:41 -04:00
Ryan
41ade2e205 refactor/fix: api redoc 2025-04-26 17:31:51 -04:00
Ryan
0a9d332d60 refactor(auth): relocate logout handler to main.js 2025-04-26 04:33:01 -04:00
Ryan
1983f7705f enhance CSP for iframe and refactor gallery view event handlers 2025-04-26 04:08:56 -04:00
Ryan
6b2bf0ba70 Refactor event binding in domUtils & fileListView 2025-04-26 03:33:23 -04:00
Ryan
6d9715169c Harden security: enable CSP, add SRI, and externalize inline scripts 2025-04-26 02:28:02 -04:00
Ryan
0645a3712a Use Material icons for dark/light toggle and simplify download flows 2025-04-25 20:40:00 -04:00
Ryan
ebc32ea965 consolidate & protect API docs with php wrapper 2025-04-24 19:34:09 -04:00
Ryan
078db33458 Embed API documentation as a full-screen modal 2025-04-24 17:35:41 -04:00
Ryan
04f5cbe31f chore: update install docs, secure API docs, refine Docker vhost, remove unused folders 2025-04-24 17:02:50 -04:00
Ryan
b5a7d8d559 continue breadcrumb update 2025-04-23 23:17:23 -04:00
Ryan
58f8485b02 fix(breadcrumb): prevent XSS in title breadcrumbs – closes #24 2025-04-23 22:45:25 -04:00
Ryan
3e1da9c335 Add missing permissions in UserModel.php for TOTP login. 2025-04-23 21:15:55 -04:00
Ryan
6bf6206e1c Add missing permissions for TOTP login 2025-04-23 21:14:59 -04:00
Ryan
f9c60951c9 Removed Old CSRF logic 2025-04-23 19:53:47 -04:00
Ryan
06b3f28df0 New fetchWithCsrf with fallback for session change. start.sh session directory added. 2025-04-23 09:53:21 -04:00
Ryan
89f124250c Fixed totp isAdmin when session is missing but remember_me_token cookie present 2025-04-23 02:30:43 -04:00
Ryan
66f13fd6a7 dockerignore cleanup 2025-04-23 01:50:24 -04:00
Ryan
a81d9cb940 Enhance remember me 2025-04-23 01:47:27 -04:00
Ryan
13b8871200 docker: remove symlink add alias for uploads folder 2025-04-22 22:28:06 -04:00
Ryan
2792c05c1c docker: consolidate config & security improvements 2025-04-22 21:34:21 -04:00
Ryan
6ccfc88acb Composer & WebDAV readme changes 2025-04-22 19:27:53 -04:00
Ryan
7f1d59b33a add acknowledgements to README and LICENSE 2025-04-22 19:06:33 -04:00
Ryan
e4e8b108d2 Add permissions to workflow 2025-04-22 18:11:42 -04:00
Ryan
242661a9c9 New Admin Panel settings (enableWebDAV & shareMaxUploadSize) 2025-04-22 17:11:19 -04:00
Ryan
ca3e2f316c PUID/PGID changes 2025-04-22 08:19:10 -04:00
Ryan
6ff4aa5f34 support PUID/PGID env vars & update Unraid template 2025-04-22 08:06:29 -04:00
Ryan
1eb54b8e6e Updated WebDav and curl readme 2025-04-21 13:23:54 -04:00
Ryan
4a6c424540 Add sabre/dav to dependencies and fix resumable.js url 2025-04-21 11:57:01 -04:00
Ryan
d23d5b7f3f Added WebDAV Support & curl 2025-04-21 11:12:42 -04:00
Ryan
a48ba09f02 Add WebDAV support with user folderOnly restrictions 2025-04-21 10:39:55 -04:00
Ryan
61357af203 Fetch URL fixes, Extended “Remember Me” cookie behavior, submitLogin() overhaul 2025-04-19 17:53:01 -04:00
Ryan
e390a35e8a Gallery View add selection actions and search filtering 2025-04-18 02:58:30 -04:00
Ryan
7e50ba1f70 test pipeline 2025-04-18 00:52:39 -04:00
Ryan
cc41f8cc95 update sync 2025-04-18 00:51:51 -04:00
Ryan
7c31b9689f update changelog & test pipeline 2025-04-18 00:43:33 -04:00
Ryan
461921b7bc Remember me adjustment 2025-04-18 00:40:17 -04:00
Ryan
3b58123584 User Panel added API Docs link 2025-04-17 06:45:00 -04:00
Ryan
cd9d7eb0ba HTML wrapper that pulls in Redoc from the CDN 2025-04-17 06:28:05 -04:00
Ryan
c0c8d68dc4 mark openapi.json & api.html as documentation 2025-04-17 06:11:27 -04:00
Ryan
2dfcb4062f Generate OpenAPI spec and API HTML docs 2025-04-17 06:04:15 -04:00
Ryan
d839b3ac1c Fix folder share gallery view link 2025-04-17 02:38:32 -04:00
Ryan
766458f707 Dockerfile, custom-php.ini & start.sh moved into main repo 2025-04-17 02:10:46 -04:00
Ryan
22cce5a898 Overhaul networkUtils and expand auth 2025-04-17 01:20:18 -04:00
Ryan
75d3bf5a9b Refactor fixes and adjustments 2025-04-16 17:15:59 -04:00
Ryan
4ec4ba832f Changes 4/16 Refactor API endpoints and modularize controllers and models 2025-04-16 13:04:36 -04:00
Ryan
97b67593bc remove 2025-04-16 11:43:29 -04:00
Ryan
ec5c3fc452 Refactor API endpoints and modularize controllers and models 2025-04-16 11:40:17 -04:00
Ryan
853d8835d9 Adjust Gallery View max columns based on screen size & Adjust headerTitle to update globally 2025-04-15 16:07:20 -04:00
Ryan
1d36d002c6 Force resumable chunk size & fix chunk cleanup 2025-04-14 16:58:12 -04:00
Ryan
844976ef89 Updated zoom-in, zoom-out, rotate-left & rotate-right buttons to match prev & next 2025-04-14 13:06:30 -04:00
Ryan
66e0d7ecbe filePreview enhancements and ensure gallery view toggle displays 2025-04-14 03:42:24 -04:00
Ryan
a5fbcdef88 Fix Gallery View: medium screen devices get 3 max columns and small screen devices 2 max columns. 2025-04-14 00:29:30 -04:00
Ryan
a897d1734f Gallery view enhancements 2025-04-13 20:48:34 -04:00
Ryan
a9c4200827 Extend i18n support: Add new translation keys for Download and Share modals 2025-04-13 18:38:21 -04:00
Ryan
97559873dc Added translations and data attributes for almost all user-facing text 2025-04-13 15:22:52 -04:00
Ryan
0683b27534 New Admin section Header Settings to change Header Title. 2025-04-13 05:51:19 -04:00
Ryan
49c42e8096 RecoveryCodeModal i18n 2025-04-13 04:17:32 -04:00
Ryan
ed39e112a9 more i18n.js keys added 2025-04-13 03:55:51 -04:00
Ryan
25edab923a Decreased header size some more and clickable logo 2025-04-13 02:52:14 -04:00
Ryan
b8ae3c4402 Advanced/Basic search button as material icon on same row as search bar. 2025-04-12 16:31:05 -04:00
Ryan
fb537b1d61 Change search box text when enabling advanced search. 2025-04-12 15:36:23 -04:00
Ryan
90439022e3 Gallery View Toggle button moved to header 2025-04-12 15:16:59 -04:00
Ryan
b4c8738b8a advance search readme added 2025-04-12 14:18:14 -04:00
Ryan
e193bf9b13 Advanced Search Implementation 2025-04-12 14:16:52 -04:00
Ryan
a70d8fc2c7 v1.1.2 2025-04-12 12:52:15 -04:00
Ryan
d9f69d7917 Fuse.js Integration for Indexed Real-Time Searching & Dependencies added 2025-04-12 12:46:28 -04:00
Ryan
28ac23c2f6 Fix totp_setup.php to use header-based CSRF token verification 2025-04-11 23:40:27 -04:00
Ryan
b06c49f213 ensure consistent session behavior 2025-04-11 22:36:43 -04:00
Ryan
8553efabc1 Upgrade dependencies: update robthree/twofactorauth to v3 and endroid/qr-code to v5; update TOTP integration (namespace, enum, QR provider) accordingly 2025-04-11 18:41:44 -04:00
Ryan
81a08ffd5b fix missing 2025-04-11 10:55:33 -04:00
Ryan
296dae96a5 regex configuration constants 2025-04-11 10:44:26 -04:00
Ryan
337f529afd fix drag-drop, UI glitches, & update validation 2025-04-11 03:21:09 -04:00
Ryan
4360f2830a new filetag endpoint and config.php update 2025-04-10 10:33:13 -04:00
Ryan
894cc938a5 i18n-title for folder buttons 2025-04-10 04:27:40 -04:00
Ryan
01801ba950 fix gallery view filename wrapping 2025-04-10 03:58:26 -04:00
Ryan
5b592575a4 adjust card mouseover start position 2025-04-10 03:18:08 -04:00
Ryan
7cce03d092 Use create folder material icon and increase version 2025-04-10 02:59:04 -04:00
Ryan
ff92a6d26c Reduce header height & create folder material icon 2025-04-10 02:29:48 -04:00
Ryan
4fa5faa2bf Shift Key Multi‑Selection & Total Files and File Size 2025-04-10 00:45:35 -04:00
Ryan
98850a7c65 Update 2025-04-09 02:20:16 -04:00
Ryan
15bac15c33 update readme 2025-04-09 01:48:56 -04:00
Ryan
b2ff3efb3b Folder sharing added 2025-04-09 01:46:07 -04:00
Ryan
b9ce3f92a4 Progress modal for handleExtractZip 2025-04-08 21:04:44 -04:00
Ryan
f65b151bc3 Progress Modal on download buttons 2025-04-08 20:30:17 -04:00
Ryan
703c93db25 semi-complete internationalization 2025-04-08 18:49:30 -04:00
Ryan
d0353b137b German translation added 2025-04-08 18:48:22 -04:00
Ryan
a6c4c1d39c Start i18n Integration 2025-04-08 18:40:01 -04:00
Ryan
7aa4fe142a readme update for user permissions 2025-04-08 02:08:10 -04:00
Ryan
9f8337574a v1.0.9 2025-04-07 23:35:35 -04:00
Ryan
82eadebe3b adjust comments 2025-04-07 23:26:37 -04:00
Ryan
9701747214 fix(security): mitigate CodeQL alerts by adding SRI attributes and sanitizing DOM content 2025-04-07 23:15:06 -04:00
Ryan
6ff25ed426 more changes 2025-04-07 12:10:39 -04:00
Ryan
ecc41bfe31 user permissions 2025-04-07 12:08:58 -04:00
Ryan
94055d2c92 Update issue templates 2025-04-07 02:39:54 -04:00
Ryan
5b50400f28 more readme adjustments 2025-04-07 02:20:46 -04:00
Ryan
688a4bcf52 adjustments 2025-04-07 02:07:04 -04:00
Ryan
4bcbb08650 filerise.net emails 2025-04-07 01:51:20 -04:00
Ryan
1a044145ab New readme, security, changelog & wiki 2025-04-07 01:32:35 -04:00
Ryan
59299cdbed readme update 2025-04-06 15:06:51 -04:00
Ryan
4f74090818 fix toggleRecovery text 2025-04-06 14:46:37 -04:00
Ryan
70163d22f0 totp one time recovery code added 2025-04-06 14:35:45 -04:00
Ryan
b4445fc4d8 share new password page, totp setup focus, logout clear session cookie 2025-04-06 01:24:13 -04:00
Ryan
4022ccde84 v1.0.8 2025-04-05 23:46:21 -04:00
Ryan
8d370fd1bb totp adjustments 2025-04-05 23:42:52 -04:00
Ryan
5100e8bf3b extend TOTP to basic auth & OIDC. Fix share btn galleryview. 2025-04-05 22:22:47 -04:00
Ryan
899b04e49a Modularize fileManager.js 2025-04-05 15:14:49 -04:00
Ryan
07053a6b9a Organize app source 2025-04-05 13:23:31 -04:00
Ryan
58db1d49ac demo.filerise.net 2025-04-04 19:27:01 -04:00
Ryan
a2d678ee19 1.0.7 2025-04-04 18:18:19 -04:00
Ryan
da62e70c02 mitigate path traversal vulnerability by validating folder and file inputs 2025-04-04 18:02:21 -04:00
Ryan
f19d30f58a demo.filerise.net 2025-04-04 16:16:21 -04:00
Ryan
a8202adbec demo.filerise.net 2025-04-04 16:16:01 -04:00
Ryan
5dc58ffa42 loadCsrfTokenWithRetry 2025-04-04 02:29:27 -04:00
Ryan
f4f700ecda Chain Initialization After CSRF Token Is Loaded 2025-04-04 02:13:00 -04:00
Ryan
94178775d5 loadUserPermissions cleanup 2025-04-04 01:58:36 -04:00
Ryan
1d3f731483 fix UserPermission missing function. 2025-04-03 23:24:43 -04:00
Ryan
6926d5b065 userPermissions issue fixed 2025-04-03 22:06:49 -04:00
Ryan
46e9761cae Add click event listener to the “oidcLoginBtn” 2025-04-03 21:43:32 -04:00
Ryan
fa828f5dea new image 2025-04-03 20:13:41 -04:00
Ryan
3a86903827 v1.0.6 2025-04-03 20:05:14 -04:00
Ryan
4feef5700d video demo2 2025-04-03 20:01:31 -04:00
Ryan
41e2b5af90 New Images 2025-04-03 19:56:04 -04:00
Ryan
27f071ba6e remove testing style 2025-04-03 16:28:55 -04:00
Ryan
9020251ed5 Header Drop Zone Extension 2025-04-03 15:12:48 -04:00
Ryan
84822e699e Fixed fileDragStartHandler to work with tagFiles. 2025-04-02 16:05:29 -04:00
Ryan
3d57efba6c Allow mkv video playback if supported and custom toast opacity increased 2025-04-02 14:41:58 -04:00
Ryan
7c3ce0803a User Permissions (User Folder, Read Only, Disable Upload) & more changes 2025-04-02 04:49:36 -04:00
Ryan
119aefc209 adjust chunk merging logic 2025-03-31 05:44:19 -04:00
Ryan
52ddf8268f Implemented Video Progress Saving and Resuming 2025-03-31 05:33:52 -04:00
Ryan
8d7187d538 File Tagging and Global Tag Management added 2025-03-31 04:41:55 -04:00
Ryan
394e7ef041 animate merging chunks 2025-03-30 17:31:27 -04:00
Ryan
9c71c46c4e remove chunk folder php RecursiveDirectoryIterator 2025-03-30 16:49:00 -04:00
Ryan
d228dc10b0 Admin panel move user management to top 2025-03-30 15:08:59 -04:00
Ryan
3f1007b1b3 New User Panel with TOTP & change password. Admin Panel added Global OTPAuth URL 2025-03-30 14:44:53 -04:00
Ryan
27de0a9a48 composer dependencies for OIDC 2025-03-29 04:47:21 -04:00
Ryan
051544dc5a New Admin Panel, OIDC Integration & Card dragDrop adjustments 2025-03-29 04:33:10 -04:00
Ryan
89777584cf Merge pull request #5 from BitOlli/patch-1
Readme fixes
2025-03-28 11:59:19 -04:00
BitOlli
ed47e3c3bc Update README.md
fixed PR, typo
2025-03-28 15:30:20 +01:00
BitOlli
edd9094218 Update README.md
corrected docker-compose.yml section on readme.md to have proper YAML indentation
2025-03-28 15:27:33 +01:00
Ryan
3b0083516b Additional audio formats to GalleryView 2025-03-27 13:51:15 -04:00
Ryan
fee3b544dd basic auth 2025-03-27 13:43:51 -04:00
Ryan
99ed05d3de basic auth login 2025-03-27 13:39:47 -04:00
Ryan
32469778dc audio files playback added 2025-03-27 13:14:14 -04:00
Ryan
ecb4ac2c75 update 2025-03-27 11:30:21 -04:00
Ryan
4ae509acd2 Contributing added 2025-03-27 11:28:45 -04:00
Ryan
b1cd4b7bdc contribute md 2025-03-27 11:23:52 -04:00
Ryan
d57687adee new video demo 2025-03-27 05:16:50 -04:00
Ryan
64d41af21b Show extracted names in toast and new images 2025-03-27 05:11:40 -04:00
Ryan
a8f5a6d3bc Double root empty folder fix, side bar drag zone adjusted 2025-03-27 03:22:43 -04:00
Ryan
062cfc0dd4 FileRise changes 2025-03-26 18:55:27 -04:00
Ryan
32d25b1b69 FileRise changes 2025-03-26 18:54:54 -04:00
Ryan
56626aaa40 change to FileRise round 1 2025-03-26 18:26:57 -04:00
Ryan
0697fcb1df New Name FileRise, logo rises, DragDrop Upload & Folder cards 2025-03-26 18:23:42 -04:00
Ryan
c08c903810 Adjust context menu to stay in viewport 2025-03-25 04:23:37 -04:00
Ryan
2c8374a66c New features added 2025-03-25 03:36:44 -04:00
Ryan
49138835ce Authentication & Initialization Changes plus File & Fold Manager Enhancements 2025-03-25 03:29:32 -04:00
Ryan
c0dc0ce391 Rename file modal select and focus filename 2025-03-24 16:53:37 -04:00
Ryan
6426f4b924 Redirection 2025-03-24 16:36:32 -04:00
Ryan
b72356b657 attachEnterKeyListener, focus and showCustomConfirmModal added 2025-03-24 13:46:35 -04:00
Ryan
fc45767712 Save admin status in persistent token 2025-03-24 10:21:20 -04:00
Ryan
1d5c6a48b5 PERSISTENT_TOKENS_KEY updates 2025-03-24 00:16:09 -04:00
Ryan
772326c8e0 Added PERSISTENT_TOKENS_KEY to Using Docker Compose: 2025-03-24 00:08:06 -04:00
Ryan
5892236aa9 encrypt and decrypt persistent tokens 2025-03-23 23:29:51 -04:00
Ryan
0215bd3d76 add highlight to pauseResumeBtn 2025-03-23 02:43:16 -04:00
Ryan
a9c7bb6493 Resumable.js Integration (pause, resume, retry & chunked uploads) 2025-03-23 02:13:10 -04:00
Ryan
6d588eb143 Folder Breadcrumb Rendering with Drag-and-Drop support 2025-03-22 08:17:34 -04:00
Ryan
2092512f43 Add 30day persistent token remember me for login 2025-03-22 07:12:20 -04:00
Ryan
833eaa3194 gallery view default icon fixed 2025-03-22 06:42:00 -04:00
Ryan
edb8ff476a added brute force protection 2025-03-22 05:44:56 -04:00
Ryan
2e55f5f4d7 change password to readme 2025-03-21 18:08:51 -04:00
Ryan
ae48119e15 adjusted readme 2025-03-21 17:46:12 -04:00
Ryan
f5410a92e7 adjusted readme 2025-03-21 17:42:45 -04:00
Ryan
7898ad4f1c folder dash adjusted 2025-03-21 17:38:44 -04:00
Ryan
d3ce26e83d additional readme info 2025-03-21 16:23:39 -04:00
Ryan
b4a903e738 MFE - Multi File Upload Editor 2025-03-21 16:19:00 -04:00
Ryan
53bb72f4ab change password added 2025-03-21 15:46:55 -04:00
Ryan
cef96f0047 Clean up expired share links 2025-03-21 14:17:25 -04:00
Ryan
559df3c396 Updated token generation to 16 bytes (32 hex characters) 2025-03-21 14:05:44 -04:00
Ryan
a24321455a block php script execution 2025-03-21 11:17:24 -04:00
Ryan
3c2faa5218 adjusted readme 2025-03-21 11:15:13 -04:00
172 changed files with 25169 additions and 6137 deletions

14
.dockerignore Normal file
View 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
View File

@@ -0,0 +1,4 @@
public/api.html linguist-documentation
public/openapi.json linguist-documentation
resources/ export-ignore
.github/ export-ignore

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

10
.github/ISSUE_TEMPLATE/custom.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

43
.github/workflows/sync-changelog.yml vendored Normal file
View 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

1399
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

243
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,243 @@
# Contributing to FileRise
Thank you for your interest in contributing to FileRise! We appreciate your help in making this self-hosted file manager even better.
## Table of Contents
- [Getting Started](#getting-started)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Pull Requests](#pull-requests)
- [Coding Guidelines](#coding-guidelines)
- [Documentation](#documentation)
- [Questions and Support](#questions-and-support)
- [Adding New Language Translations](#adding-new-language-translations)
## Getting Started
1. **Fork the Repository**
Click the **Fork** button on the top-right of the FileRise GitHub page to create your own copy.
2. **Clone Your Fork**
```bash
git clone https://github.com/yourusername/FileRise.git
cd FileRise
```
3. **Set Up a Local Environment**
FileRise runs on a standard LAMP stack. Ensure you have PHP, Apache, and the necessary dependencies installed.
4. **Configuration**
Copy any example configuration files (if provided) and adjust them as needed for your local setup.
## Reporting Bugs
If you discover a bug, please open an issue on GitHub and include:
- A clear and descriptive title.
- Detailed steps to reproduce the bug.
- The expected and actual behavior.
- Screenshots or error logs (if applicable).
- Environment details (PHP version, Apache version, OS, etc.).
## Suggesting Enhancements
Have an idea for a new feature or improvement? Before opening a new issue, please check if a similar suggestion already exists. If not, open an issue with:
- A clear description of the enhancement.
- Use cases or examples of how it would be beneficial.
- Any potential drawbacks or alternatives.
## Pull Requests
We welcome pull requests! To submit one, please follow these guidelines:
1. **Create a New Branch**
Always create a feature branch from master.
```bash
git checkout -b feature/your-feature-name
```
2. **Make Your Changes**
Commit your changes with clear, descriptive messages. Make sure your code follows the projects style guidelines.
3. **Write Tests**
If applicable, add tests to cover your changes to help us maintain code quality.
4. **Submit the Pull Request**
Push your branch to your fork and open a pull request against the master branch in the main repository. Provide a detailed description of your changes and why theyre needed.
## Coding Guidelines
- **Code Style:**
Follow the conventions used in the project. Consistent indentation, naming conventions, and clear code organization are key.
- **Documentation:**
Update documentation if your changes affect the usage or configuration of FileRise.
- **Commit Messages:**
Write meaningful commit messages that clearly describe the purpose of your changes.
## Documentation
If you notice any areas in the documentation that need improvement or updating, please feel free to include those changes in your pull requests. Clear documentation is essential for helping others understand and use FileRise.
## Questions and Support
If you have any questions, ideas, or need support, please open an issue or join our discussion on [GitHub Discussions](https://github.com/error311/FileRise/discussions). Were here to help and appreciate your contributions.
## Adding New Language Translations
FileRise supports internationalization (i18n) and localization via a central translation file (`i18n.js`). If you would like to contribute a new language translation, please follow these steps:
1. **Update `i18n.js`:**
Open the `i18n.js` file located in the `js` directory. Within the `translations` object, add a new property using the appropriate [ISO language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) as the key. Copy the structure from an existing language block and translate each key.
**Example (for German):**
```js
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"
}
---
Thank you for helping to improve FileRise and happy coding!

140
Dockerfile Normal file
View File

@@ -0,0 +1,140 @@
# 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
# 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"]

View File

@@ -1,6 +1,7 @@
MIT License
Copyright (c) 2024 SeNS
Copyright (c) 2025 FileRise
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

403
README.md
View File

@@ -1,257 +1,256 @@
# Multi File Upload Editor
# FileRise
https://github.com/user-attachments/assets/179e6940-5798-4482-9a69-696f806c37de
**Elevate your File Management** A modern, self-hosted web file manager.
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.
changelogs available here: <https://github.com/error311/multi-file-upload-editor-docker/>
**4/3/2025 Video demo:**
Multi File Upload Editor is a lightweight, secure, self-hosted web application for uploading, editing, and managing files. Built with an Apache/PHP backend and a modern JavaScript (ES6 modules) frontend, it offers a responsive, dynamic file management interface. It serves as an alternative to solutions like FileGator or ProjectSend, providing an easy-to-setup experience ideal for document management, image galleries, firmware file hosting, and more.
<https://github.com/user-attachments/assets/221f6a53-85f5-48d4-9abe-89445e0af90e>
**Dark mode:**
![Dark Header](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-header.png)
---
## Features
## Features at a Glance or [Full Features Wiki](https://github.com/error311/FileRise/wiki/Features)
- **Multiple File/Folder Uploads with Progress:**
- Users can select and upload multiple files & folders at once.
- Each file upload displays an individual progress bar with percentage and upload speed.
- Image files show a small thumbnail preview (with default Material icons for other file types).
- **Built-in File Editing & Renaming:**
- Text-based files (e.g., .txt, .html, .js) can be opened and edited in a modal window using CodeMirror for:
- Syntax highlighting
- Line numbering
- Adjustable font sizes
- Files can be renamed directly through the interface.
- The renaming functionality now supports names with parentheses and checks for duplicate names, automatically generating a unique name (e.g., appending “ (1)”) when needed.
- Folder-specific metadata is updated accordingly.
- **Built-in File Preview:**
- Users can quickly preview images, videos, and PDFs directly in modal popups without leaving the page.
- The preview modal supports inline display of images (with proper scaling) and videos with playback controls.
- Navigation (prev/next) within image previews is supported for a seamless browsing experience.
- **Gallery (Grid) View:**
- In addition to the traditional table view, users can toggle to a gallery view that arranges image thumbnails in a grid layout.
- The gallery view offers multiple column options (e.g., 3, 4, or 5 columns) so that users can choose the layout that best fits their screen.
- Action buttons (Download, Edit, Rename, Share) appear beneath each thumbnail for quick access.
- **Batch Operations (Delete/Copy/Move/Download):**
- **Delete Files:** Delete multiple files at once.
- **Copy Files:** Copy selected files to another folder with a unique-naming feature to prevent overwrites.
- **Move Files:** Move selected files to a different folder, automatically generating a unique filename if needed to avoid data loss.
- **Download Files as ZIP:** Download selected files as a ZIP archive. Users can specify a custom name for the ZIP file via a modal dialog.
- **Drag & Drop:** Easily move files by selecting them from the file list and simply dragging them onto your desired folder in the folder tree. When you drop the files onto a folder, the system automatically moves them, updating your file organization in one seamless action.
- **Folder Management:**
- Organize files into folders and subfolders with the ability to create, rename, and delete folders.
- A dynamic folder tree in the UI allows users to navigate directories easily, and any changes are immediately reflected in real time.
- **Per-Folder Metadata Storage:** Each folder has its own metadata JSON file (e.g., `root_metadata.json`, `FolderName_metadata.json`), and operations (copy/move/rename) update these metadata files accordingly.
- **Sorting & Pagination:**
- The file list can be sorted by name, modified date, upload date, file size, or uploader.
- Pagination controls let users navigate through files with selectable page sizes (10, 20, 50, or 100 items per page) and “Prev”/“Next” navigation buttons.
- **Share Link Functionality:**
- Generate shareable links for files with configurable expiration times (e.g., 30, 60, 120, 180, 240 minutes, and a 1-day option) and optional password protection.
- Share links are stored in a JSON file with details including the folder, file, expiration timestamp, and hashed password.
- The share endpoint (`share.php`) validates tokens, expiration, and password before serving files (or forcing downloads).
- The share URL is configurable via environment variables or auto-detected from the server.
- **User Authentication & Management:**
- Secure, session-based authentication protects the file manager.
- Admin users can add or remove users through the interface.
- Passwords are hashed using PHPs `password_hash()` for security.
- All state-changing endpoints include CSRF token validation.
- **Responsive, Dynamic & Persistent UI:**
- The interface is mobile-friendly and adapts to various screen sizes by hiding non-critical columns on small devices.
- Asynchronous updates (via Fetch API and XMLHttpRequest) keep the UI responsive without full page reloads.
- Persistent settings (such as items per page, dark/light mode preference, folder tree state, and the last open folder) ensure a smooth and customized user experience.
- **Dark Mode/Light Mode:**
- The application automatically adapts to the operating systems theme preference by default and offers a manual toggle.
- The dark mode provides a darker background with lighter text and adjusts UI elements (including the CodeMirror editor) for optimal readability in low-light conditions.
- The light mode maintains a bright interface for well-lit environments.
- **Server & Security Enhancements:**
- The Apache configuration (or .htaccess files) is set to disable directory indexing (e.g., using `Options -Indexes` in the uploads directory), preventing unauthorized users from viewing directory contents.
- Direct access to sensitive files (e.g., `users.txt`) is restricted through .htaccess rules.
- A proxy download mechanism has been implemented (via endpoints like `download.php` and `downloadZip.php`) so that every file download request goes through a PHP script. This script validates the session and CSRF token before streaming the file, ensuring that even if a file URL is guessed, only authenticated users can access it.
- Administrators are advised to deploy the app on a secure internal network or use the proxy download mechanism for public deployments to further protect file content.
- **Trash Management with Restore & Delete:**
- **Trash Storage & Metadata:**
- Deleted files are moved to a designated “Trash” folder rather than being immediately removed.
- Metadata is stored in a JSON file (`trash.json`) that records:
- Original folder and file name
- Timestamp when the file was trashed
- Uploader information (and optionally who deleted it)
- Additional metadata (e.g., file type)
- **Restore Functionality:**
- Admins can view trashed files in a modal.
- They can restore individual files (with conflict checks) or restore all files back to their original location.
- **Delete Functionality:**
- Users can permanently delete trashed files via:
- **Delete Selected:** Remove specific files from the Trash and update `trash.json`.
- **Delete All:** Permanently remove every file from the Trash after confirmation.
- **Auto-Purge Mechanism:**
- The system automatically purges (permanently deletes) any files in the Trash older than three days, helping manage storage and prevent the accumulation of outdated files.
- **User Interface:**
- The trash modal displays details such as file name, uploader/deleter, and the trashed date/time.
- Material icons with tooltips visually represent the restore and delete actions.
- 🚀 **Easy File Uploads:** Upload multiple files and folders via drag & drop or file picker. Supports large files with pause/resumable chunked uploads and shows real-time progress for each file. No more failed transfers FileRise will pick up where it left off if your connection drops.
- 🗂️ **File Management:** Full set of file/folder operations move or copy files (via intuitive drag-drop or dialogs), rename items, and delete in batches. You can even download selected files as a ZIP archive or extract uploaded ZIP files server-side. Organize content with an interactive folder tree and breadcrumb navigation for quick jumps.
- 🗃️ **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 headless 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)) quickstart for examples. FolderOnly users are restricted to their personal directory, while admins and unrestricted users have full access.
- 📚 **API Documentation:** Fully autogenerated 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 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, 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 & SelfContained:** FileRise runs on PHP 8.1+ with no external database required data is stored in files (users, metadata) for simplicity. Its a singlefolder web app you can drop into any Apache/PHP server or run as a container. Docker & Unraid ready: use our prebuilt image for a hasslefree 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.)
---
## Screenshots
## Live Demo
**Light mode**
![Light Mode](https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/light-mode.png)
**Dark mode**
![Dark Mode](https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/dark-mode.png)
**Dark editor**
![dark-editor](https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/dark-editor.png)
**Dark preview**
![dark-preview](https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/dark-preview.png)
**Restore or Delete Trash**
![restore-delete](https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/restore-delete.png)
**Login page**
![Login](https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/login-page.png)
**iphone screenshots:**
<p align="center">
<img src="https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/dark-iphone.png" width="45%">
<img src="https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/light-preview-iphone.png" width="45%">
</p>
Curious about the UI? **Check out the live demo:** <https://demo.filerise.net> (login with username “demo” and password “demo”). *The demo is read-only for security*. Explore the interface, switch themes, preview files, and see FileRise in action!
---
## Installation & Setup
### Manual Installation
You can deploy FileRise either by running the **Docker container** (quickest way) or by a **manual installation** on a PHP web server. Both methods are outlined below.
1. **Clone or Download the Repository:**
- **Clone:**
### 1. Running with Docker (Recommended)
```bash
git clone https://github.com/error311/multi-file-upload-editor.git
```
If you have Docker installed, you can get FileRise up and running in minutes:
- **Download:**
Download the latest release from the GitHub releases page and extract it into your desired directory.
- **Pull the image from Docker Hub:**
2. **Deploy to Your Web Server:**
- Place the project files in your Apache web directory (e.g., `/var/www/html`).
- Ensure PHP 8.1+ is installed along with the required extensions (php-json, php-curl, php-zip, etc.).
``` bash
docker pull error311/filerise-docker:latest
```
3. **Directory Setup & Permissions:**
- Create the following directories if they do not exist, and set appropriate permissions:
- `uploads/` for file storage.
- `users/` to store `users.txt` (user authentication data).
- `metadata/` for storing `file_metadata.json` and other metadata.
- Example commands:
- **Run a container:**
```bash
mkdir -p /var/www/uploads /var/www/users /var/www/metadata
chmod -R 775 /var/www/uploads /var/www/users /var/www/metadata
```
``` bash
docker run -d \
-p 8080:80 \
-e TIMEZONE="America/New_York" \
-e TOTAL_UPLOAD_SIZE="5G" \
-e SECURE="false" \
-v ~/filerise/uploads:/var/www/uploads \
-v ~/filerise/users:/var/www/users \
-v ~/filerise/metadata:/var/www/metadata \
--name filerise \
error311/filerise-docker:latest
```
4. **Configure Apache:**
- Ensure that directory indexing is disabled (using `Options -Indexes` in your `.htaccess` or Apache configuration).
- Make sure the Apache configuration allows URL rewriting if needed.
This will start FileRise on port 8080. Visit `http://your-server-ip:8080` to access it. Environment variables shown above are optional for instance, set `SECURE="true"` to enforce HTTPS (assuming you have SSL at proxy level) and adjust `TIMEZONE` as needed. The volume mounts ensure your files and user data persist outside the container.
5. **Configuration File:**
- Open `config.php` and adjust the following constants as necessary:
- `BASE_URL`: Set this to your web apps base URL.
- `UPLOAD_DIR`: Adjust the directory path for uploads.
- `TIMEZONE`: Set to your preferred timezone.
- `TOTAL_UPLOAD_SIZE`: Ensure it matches PHPs `upload_max_filesize` and `post_max_size` settings in your `php.ini`.
- **Using Docker Compose:**
Alternatively, use **docker-compose**. Save the snippet below as docker-compose.yml and run `docker-compose up -d`:
### Initial Setup Instructions
``` yaml
version: '3'
services:
filerise:
image: error311/filerise-docker:latest
ports:
- "8080:80"
environment:
TIMEZONE: "UTC"
TOTAL_UPLOAD_SIZE: "10G"
SECURE: "false"
PERSISTENT_TOKENS_KEY: "please_change_this_@@"
volumes:
- ./uploads:/var/www/uploads
- ./users:/var/www/users
- ./metadata:/var/www/metadata
```
- **First Launch Admin Setup:**
On first launch, if no users exist, the application will enter a setup mode. You will be prompted to create an admin user. This is handled automatically by the application (e.g., via a “Create Admin” form).
**Note:** No default credentials are provided. You must create the first admin account to log in and manage additional users.
FileRise will be accessible at `http://localhost:8080` (or your servers IP). The above example also sets a custom `PERSISTENT_TOKENS_KEY` (used to encrypt “remember me” tokens) be sure to change it to a random string for security.
**First-time Setup:** On first launch, FileRise will detect no users and prompt you to create an **Admin account**. Choose your admin username & password, and youre in! You can then head to the **User Management** section to add additional users if needed.
### 2. Manual Installation (PHP/Apache)
If you prefer to run FileRise on a traditional web server (LAMP stack or similar):
- **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 servers directory (e.g., `/var/www/`). It can be in a subfolder (just adjust the `BASE_URL` in config as below).
- **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 dont exist):
``` bash
mkdir -p uploads users metadata
chown -R www-data:www-data uploads users metadata # www-data is Apache user; use appropriate user
chmod -R 775 uploads users metadata
```
The uploads/ folder is where files go, users/ stores the user credentials file, and metadata/ holds metadata like tags and share links.
- **Configuration:** Open the `config.php` file in a text editor. You may want to adjust:
- `BASE_URL` the URL where you will access FileRise (e.g., `“https://files.mydomain.com/”`). This is used for generating share links.
- `TIMEZONE` and `DATE_TIME_FORMAT` match your locale (for correct timestamps).
- `TOTAL_UPLOAD_SIZE` max aggregate upload size (default 5G). Also adjust PHPs `upload_max_filesize` and `post_max_size` to at least this value (the Docker start script auto-adjusts PHP limits).
- `PERSISTENT_TOKENS_KEY` set a unique secret if you use “Remember Me” logins, to encrypt the tokens.
- Other settings like `UPLOAD_DIR`, `USERS_FILE` etc. generally dont need changes unless you move those folders. Defaults are set for the directories mentioned above.
- **Web Server Config:** If using Apache, ensure `.htaccess` files are allowed or manually add the rules from `.htaccess` to your Apache config these disable directory listings and prevent access to certain files. For Nginx or others, youll need to replicate those protections (see Wiki: [Nginx Setup for examples](https://github.com/error311/FileRise/wiki/Nginx-Setup)). Also enable mod_rewrite if not already, as FileRise may use pretty URLs for share links.
Now navigate to the FileRise URL in your browser. On first load, youll be prompted to create the Admin user (same as Docker setup). After that, the application is ready to use!
---
## Docker Usage
## Quickstart: Mount via WebDAV
For users who prefer containerization, a Docker image is available
Once FileRise is running, you must enable WebDAV in admin panel to access it.
### Quickstart
```bash
# Linux (GVFS/GIO)
gio mount dav://demo@your-host/webdav.php/
1. **Pull the Docker Image:**
# macOS (Finder → Go → Connect to Server…)
dav://demo@your-host/webdav.php/
```bash
docker pull error311/multi-file-upload-editor-docker:latest
```
```
2. **Run the Container:**
### Windows (File Explorer)
```bash
docker run -d \
-p 80:80 \
-e TIMEZONE="America/New_York" \
-e TOTAL_UPLOAD_SIZE="5G" \
-e SECURE="false" \
-v /path/to/your/uploads:/var/www/uploads \
-v /path/to/your/users:/var/www/users \
-v /path/to/your/metadata:/var/www/metadata \
--name multi-file-upload-editor \
error311/multi-file-upload-editor-docker:latest
```
- Open **File Explorer** → Right-click **This PC** → **Map network drive…**
- Choose a drive letter (e.g., `Z:`).
- In **Folder**, enter:
3. **Using Docker Compose:**
```text
https://your-host/webdav.php/
```
Create a docker-compose.yml file with the following content:
- Check **Connect using different credentials**, and enter your FileRise username and password.
- Click **Finish**. The drive will now appear under **This PC**.
```yaml
version: "3.8"
services:
web:
image: error311/multi-file-upload-editor-docker:latest
ports:
- "80:80"
environment:
TIMEZONE: "America/New_York"
TOTAL_UPLOAD_SIZE: "5G"
SECURE: "false"
volumes:
- /path/to/your/uploads:/var/www/uploads
- /path/to/your/users:/var/www/users
- /path/to/your/metadata:/var/www/metadata
```
> **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.
**Then start the container with:**
```bash
docker-compose up -d
```
📖 For a full guide (including SSL setup, HTTP workaround, and troubleshooting), see the [WebDAV Usage Wiki](https://github.com/error311/FileRise/wiki/WebDAV).
---
## Configuration Guidance
## FAQ / Troubleshooting
The `config.php` file contains several key constants that may need adjustment for your deployment:
- **“Upload failed” or large files not uploading:** Make sure `TOTAL_UPLOAD_SIZE` in config and PHPs `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.
- **BASE_URL:**
Set to the URL where your application is hosted (e.g., `http://yourdomain.com/uploads/`).
- **How to enable HTTPS?** FileRise itself doesnt handle TLS. Run it behind a reverse proxy like Nginx, Caddy, or Apache with SSL, or use Docker with a companion like nginx-proxy or Caddy. Set `SECURE="true"` env var in Docker so FileRise knows to generate https links.
- **UPLOAD_DIR, USERS_DIR, META_DIR:**
Define the directories for uploads, user data, and metadata. Adjust these to match your server environment or Docker volume mounts.
- **Changing Admin or resetting password:** Admin can change any users password via the UI (User Management section). If you lose admin access, you can edit the `users/users.txt` file on the server passwords are hashed (bcrypt), but you can delete the admin line and then restart the app to trigger the setup flow again.
- **TIMEZONE & DATE_TIME_FORMAT:**
Set according to your regional settings.
- **Where are my files stored?** In the `uploads/` directory (or the path you set for `UPLOAD_DIR`). Within it, files are organized in the folder structure you see in the app. Deleted files move to `uploads/trash/`. Tag information is in `metadata/file_metadata`.json and trash metadata in `metadata/trash.json`, etc. Regular backups of these folders is recommended if the data is important.
- **TOTAL_UPLOAD_SIZE:**
Defines the maximum upload size (default is `5G`). Ensure that PHPs `upload_max_filesize` and `post_max_size` in your `php.ini` are consistent with this setting. The startup script (`start.sh`) updates PHP limits at runtime based on this value.
- **Updating FileRise:** If using Docker, pull the new image and recreate the container. For manual installs, download the latest release and replace the files (preserve your `config.php` and the uploads/users/metadata folders). Clear your browser cache if you have issues after an update (in case CSS/JS changed).
- **Environment Variables (Docker):**
The Docker image supports overriding configuration via environment variables. For example, you can set `SECURE`, `SHARE_URL`, and port settings via the containers environment.
For more Q&A or to ask for help, please check the Discussions or open an issue.
---
## Additional Information
## Contributing
- **Security:**
All state-changing endpoints use CSRF token validation. Ensure that sessions and tokens are correctly configured as per your deployment environment.
Contributions are welcome! If you have ideas for new features or have found a bug, feel free to open an issue. Check out the [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. You can also join the conversation in GitHub Discussions or on Reddit (see links below) to share feedback and suggestions.
- **Permissions:**
Both manual and Docker installations include steps to ensure that file and directory permissions are set correctly for the web server to read and write as needed.
Areas where you can help: translations, bug fixes, UI improvements, or building integration with other services. If you like FileRise, giving the project a ⭐ star ⭐ on GitHub is also a much-appreciated contribution!
- **Logging & Troubleshooting:**
Check Apache logs (located in `/var/log/apache2/`) for troubleshooting any issues during deployment or operation.
---
Enjoy using the Multi File Upload Editor! For any issues or contributions, please refer to the [GitHub repository](https://github.com/error311/multi-file-upload-editor).
## 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).
- **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 youre free to use, modify, and distribute **FileRise**, with attribution. We hope you find it useful and contribute back!

33
SECURITY.md Normal file
View File

@@ -0,0 +1,33 @@
# Security Policy
## Supported Versions
FileRise is actively maintained. Only supported versions will receive security updates. For details on which versions are currently supported, please see the [Release Notes](https://github.com/error311/FileRise/releases).
## Reporting a Vulnerability
If you discover a security vulnerability, please do not open a public issue. Instead, follow these steps:
1. **Email Us Privately:**
Send an email to [security@filerise.net](mailto:security@filerise.net) with the subject line “[FileRise] Security Vulnerability Report”.
2. **Include Details:**
Provide a detailed description of the vulnerability, steps to reproduce it, and any other relevant information (e.g., affected versions, screenshots, logs).
3. **Secure Communication (Optional):**
If you wish to discuss the vulnerability securely, you can use our PGP key. You can obtain our PGP key by emailing us, and we will send it upon request.
## Disclosure Policy
- **Acknowledgement:**
We will acknowledge receipt of your report within 48 hours.
- **Resolution Timeline:**
We aim to fix confirmed vulnerabilities within 30 days. In cases where a delay is necessary, we will communicate updates to you directly.
- **Public Disclosure:**
After a fix is available, details of the vulnerability will be disclosed publicly in a way that does not compromise user security.
## Additional Information
We appreciate responsible disclosure of vulnerabilities and thank all researchers who help keep FileRise secure. For any questions related to this policy, please contact us at [admin@filerise.net](mailto:admin@filerise.net).

View File

@@ -1,86 +0,0 @@
<?php
require '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
$isSetup = (isset($_GET['setup']) && $_GET['setup'] == '1');
if ($isSetup && (!file_exists($usersFile) || 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 ($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"]);
?>

283
auth.js
View File

@@ -1,283 +0,0 @@
import { sendRequest } from './networkUtils.js';
import { toggleVisibility, showToast } from './domUtils.js';
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
import { loadFolderTree } from './folderManager.js';
function initAuth() {
// First, check if the user is already authenticated.
checkAuthentication(false).then(data => {
if (data.setup) {
window.setupMode = true;
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);
return;
}
window.setupMode = false;
if (data.authenticated) {
// User is logged in—show the main UI.
toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", true);
toggleVisibility("uploadFileForm", true);
toggleVisibility("fileListContainer", true);
document.querySelector(".header-buttons").style.visibility = "visible";
// If admin, show admin-only buttons.
if (data.isAdmin) {
const addUserBtn = document.getElementById("addUserBtn");
const removeUserBtn = document.getElementById("removeUserBtn");
if (addUserBtn) addUserBtn.style.display = "block";
if (removeUserBtn) removeUserBtn.style.display = "block";
// Create and show the restore button.
let restoreBtn = document.getElementById("restoreFilesBtn");
if (!restoreBtn) {
restoreBtn = document.createElement("button");
restoreBtn.id = "restoreFilesBtn";
restoreBtn.classList.add("btn", "btn-warning");
// Use a material icon.
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
const headerButtons = document.querySelector(".header-buttons");
if (headerButtons) {
// Insert after the third child if available.
if (headerButtons.children.length >= 4) {
headerButtons.insertBefore(restoreBtn, headerButtons.children[4]);
} else {
headerButtons.appendChild(restoreBtn);
}
}
}
restoreBtn.style.display = "block";
} else {
const addUserBtn = document.getElementById("addUserBtn");
const removeUserBtn = document.getElementById("removeUserBtn");
if (addUserBtn) addUserBtn.style.display = "none";
if (removeUserBtn) removeUserBtn.style.display = "none";
// If not admin, hide the restore button.
const restoreBtn = document.getElementById("restoreFilesBtn");
if (restoreBtn) {
restoreBtn.style.display = "none";
}
}
// Set items-per-page.
const selectElem = document.querySelector(".form-control.bottom-select");
if (selectElem) {
const stored = localStorage.getItem("itemsPerPage") || "10";
selectElem.value = stored;
}
} else {
// Do not show a toast message repeatedly during initial check.
toggleVisibility("loginForm", true);
toggleVisibility("mainOperations", false);
toggleVisibility("uploadFileForm", false);
toggleVisibility("fileListContainer", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
}
}).catch(error => {
console.error("Error checking authentication:", error);
});
// Attach login event listener once.
const authForm = document.getElementById("authForm");
if (authForm) {
authForm.addEventListener("submit", function (event) {
event.preventDefault();
const formData = {
username: document.getElementById("loginUsername").value.trim(),
password: document.getElementById("loginPassword").value.trim()
};
sendRequest("auth.php", "POST", formData, { "X-CSRF-Token": window.csrfToken })
.then(data => {
if (data.success) {
console.log("✅ Login successful. Reloading page.");
sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!");
window.location.reload();
} else {
showToast("Login failed: " + (data.error || "Unknown error"));
}
})
.catch(error => console.error("❌ Error logging in:", error));
});
}
// Attach logout event listener.
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(error => console.error("Logout error:", error));
});
// Add User functionality.
document.getElementById("addUserBtn").addEventListener("click", function () {
resetUserForm();
toggleVisibility("addUserModal", true);
});
document.getElementById("saveUserBtn").addEventListener("click", function () {
const newUsername = document.getElementById("newUsername").value.trim();
const newPassword = document.getElementById("newPassword").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); // Re-check without showing toast
} else {
showToast("Error: " + (data.error || "Could not add user"));
}
})
.catch(error => console.error("Error adding user:", error));
});
document.getElementById("cancelUserBtn").addEventListener("click", function () {
closeAddUserModal();
});
// Remove User functionality.
document.getElementById("removeUserBtn").addEventListener("click", function () {
loadUserList();
toggleVisibility("removeUserModal", true);
});
document.getElementById("deleteUserBtn").addEventListener("click", function () {
const selectElem = document.getElementById("removeUsernameSelect");
const usernameToRemove = selectElem.value;
if (!usernameToRemove) {
showToast("Please select a user to remove.");
return;
}
if (!confirm("Are you sure you want to delete user " + usernameToRemove + "?")) {
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(error => console.error("Error removing user:", error));
});
document.getElementById("cancelRemoveUserBtn").addEventListener("click", function () {
closeRemoveUserModal();
});
}
function checkAuthentication(showLoginToast = true) {
// Optionally pass a flag so we don't show a toast every time.
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);
return false;
}
window.setupMode = false;
if (data.authenticated) {
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(error => {
console.error("Error checking authentication:", error);
return false;
});
}
window.checkAuthentication = checkAuthentication;
window.changeItemsPerPage = function (value) {
localStorage.setItem("itemsPerPage", value);
const folder = window.currentFolder || "root";
if (typeof renderFileTable === "function") {
renderFileTable(folder);
}
};
document.addEventListener("DOMContentLoaded", function () {
const selectElem = document.querySelector(".form-control.bottom-select");
if (selectElem) {
const stored = localStorage.getItem("itemsPerPage") || "10";
selectElem.value = stored;
}
});
function resetUserForm() {
document.getElementById("newUsername").value = "";
document.getElementById("newPassword").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(error => console.error("Error loading user list:", error));
}
export { initAuth, checkAuthentication };

View File

@@ -1,55 +0,0 @@
<?php
require 'config.php';
header('Content-Type: application/json');
$usersFile = USERS_DIR . USERS_FILE;
// Function to authenticate user
function authenticate($username, $password)
{
global $usersFile;
if (!file_exists($usersFile)) {
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
}
}
return false;
}
// Get JSON input
$data = json_decode(file_get_contents("php://input"), true);
$username = trim($data["username"] ?? "");
$password = trim($data["password"] ?? "");
// Validate input: ensure both fields are provided.
if (!$username || !$password) {
echo json_encode(["error" => "Username and password are required"]);
exit;
}
// Validate username format: allow only letters, numbers, underscores, dashes, and spaces.
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
exit;
}
// Authenticate user
$userRole = authenticate($username, $password);
if ($userRole !== false) {
// Regenerate session ID to mitigate session fixation attacks
session_regenerate_id(true);
$_SESSION["authenticated"] = true;
$_SESSION["username"] = $username;
$_SESSION["isAdmin"] = ($userRole === "1"); // "1" indicates admin
echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]);
} else {
echo json_encode(["error" => "Invalid credentials"]);
}

View File

@@ -1,22 +0,0 @@
<?php
require '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)) === '') {
// Return JSON indicating setup mode
echo json_encode(["setup" => true]);
exit();
}
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["authenticated" => false]);
exit;
}
echo json_encode([
"authenticated" => true,
"isAdmin" => isset($_SESSION["isAdmin"]) ? $_SESSION["isAdmin"] : false
]);
?>

12
composer.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "error311/filerise",
"description": "FileRise A lightweight self-hosted file manager",
"type": "project",
"require": {
"jumbojett/openid-connect-php": "^1.0.0",
"phpseclib/phpseclib": "~3.0.7",
"robthree/twofactorauth": "^3.0",
"endroid/qr-code": "^5.0",
"sabre/dav": "^4.4"
}
}

1040
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +0,0 @@
<?php
// config.php
// Allow an environment variable to override HTTPS detection.
$envSecure = getenv('SECURE');
if ($envSecure !== false) {
// Convert the environment variable value to a boolean.
$secure = filter_var($envSecure, FILTER_VALIDATE_BOOLEAN);
} else {
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
}
$cookieParams = [
'lifetime' => 7200,
'path' => '/',
'domain' => '', // Specify your domain if needed
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax'
];
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));
}
// Define BASE_URL (this should point to where index.html is, e.g. your uploads directory)
define('BASE_URL', 'http://yourwebsite/uploads/');
// If BASE_URL is still the default placeholder, use the server's HTTP_HOST.
// Otherwise, use BASE_URL and append share.php.
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);
define('UPLOAD_DIR', '/var/www/uploads/');
define('TIMEZONE', 'America/New_York');
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
define('TOTAL_UPLOAD_SIZE', '5G');
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/');
date_default_timezone_set(TIMEZONE);
?>

208
config/config.php Normal file
View 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));
}
// Autologin 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 PHPs $_SERVER key format:
$cfgAuthHeader = 'HTTP_' . strtoupper(str_replace('-', '_', $hdr));
}
define('AUTH_BYPASS', $cfgAuthBypass);
define('AUTH_HEADER', $cfgAuthHeader);
// ─────────────────────────────────────────────────────────────────────────────
// PROXY-ONLY AUTOLOGIN 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);

View File

@@ -1,143 +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;
}
$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)]);
}
?>

View File

@@ -1,86 +0,0 @@
<?php
require '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;
}
// 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.']);
}
?>

View File

@@ -1,59 +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;
}
// Optionally, you could check if the file exists in the uploads directory here.
// Generate a secure token.
$token = bin2hex(random_bytes(4)); // 8 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 = [];
}
}
// 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
View 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

View File

@@ -1,147 +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";
$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)]);
}
?>

View File

@@ -1,89 +0,0 @@
<?php
require '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;
}
// 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.']);
}
?>

View File

@@ -1,105 +0,0 @@
<?php
session_start();
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;
?>

View File

@@ -1,308 +0,0 @@
// domUtils.js
// Basic DOM Helpers
export function toggleVisibility(elementId, shouldShow) {
const element = document.getElementById(elementId);
if (element) {
element.style.display = shouldShow ? "block" : "none";
} else {
console.error(`Element with id "${elementId}" not found.`);
}
}
export function escapeHTML(str) {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
export function toggleAllCheckboxes(masterCheckbox) {
const checkboxes = document.querySelectorAll(".file-checkbox");
checkboxes.forEach(chk => {
chk.checked = masterCheckbox.checked;
});
updateFileActionButtons(); // update buttons based on current selection
}
export function updateFileActionButtons() {
const fileListContainer = document.getElementById("fileList");
const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox");
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
const copyBtn = document.getElementById("copySelectedBtn");
const moveBtn = document.getElementById("moveSelectedBtn");
const deleteBtn = document.getElementById("deleteSelectedBtn");
const zipBtn = document.getElementById("downloadZipBtn");
if (fileCheckboxes.length === 0) {
if (copyBtn) copyBtn.style.display = "none";
if (moveBtn) moveBtn.style.display = "none";
if (deleteBtn) deleteBtn.style.display = "none";
if (zipBtn) zipBtn.style.display = "none";
} else {
if (copyBtn) copyBtn.style.display = "inline-block";
if (moveBtn) moveBtn.style.display = "inline-block";
if (deleteBtn) deleteBtn.style.display = "inline-block";
if (zipBtn) zipBtn.style.display = "inline-block";
if (selectedCheckboxes.length > 0) {
if (copyBtn) copyBtn.disabled = false;
if (moveBtn) moveBtn.disabled = false;
if (deleteBtn) deleteBtn.disabled = false;
if (zipBtn) zipBtn.disabled = false;
} else {
if (copyBtn) copyBtn.disabled = true;
if (moveBtn) moveBtn.disabled = true;
if (deleteBtn) deleteBtn.disabled = true;
if (zipBtn) zipBtn.disabled = true;
}
}
}
export function showToast(message, duration = 3000) {
const toast = document.getElementById("customToast");
if (!toast) {
console.error("Toast element not found");
return;
}
toast.textContent = message;
toast.style.display = "block";
// Force reflow for transition effect.
void toast.offsetWidth;
toast.classList.add("show");
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => {
toast.style.display = "none";
}, 500);
}, duration);
}
// --- DOM Building Functions for File Table ---
export function buildSearchAndPaginationControls({ currentPage, totalPages, searchTerm }) {
const safeSearchTerm = escapeHTML(searchTerm);
return `
<div class="row align-items-center mb-3">
<div class="col-12 col-md-8 mb-2 mb-md-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" id="searchIcon">
<i class="material-icons">search</i>
</span>
</div>
<input type="text" id="searchInput" class="form-control" placeholder="Search files..." value="${safeSearchTerm}" aria-describedby="searchIcon">
</div>
</div>
<div class="col-12 col-md-4 text-left">
<div class="d-flex justify-content-center justify-content-md-start align-items-center">
<button class="custom-prev-next-btn" ${currentPage === 1 ? "disabled" : ""} onclick="changePage(${currentPage - 1})">Prev</button>
<span class="page-indicator">Page ${currentPage} of ${totalPages || 1}</span>
<button class="custom-prev-next-btn" ${currentPage === totalPages ? "disabled" : ""} onclick="changePage(${currentPage + 1})">Next</button>
</div>
</div>
</div>
`;
}
export function buildFileTableHeader(sortOrder) {
return `
<table class="table">
<thead>
<tr>
<th class="checkbox-col"><input type="checkbox" id="selectAll" onclick="toggleAllCheckboxes(this)"></th>
<th data-column="name" class="sortable-col">File Name ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="modified" class="hide-small sortable-col">Date Modified ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="uploaded" class="hide-small hide-medium sortable-col">Upload Date ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="size" class="hide-small sortable-col">File Size ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="uploader" class="hide-small hide-medium sortable-col">Uploader ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th>Actions</th>
</tr>
</thead>
`;
}
export function buildFileTableRow(file, folderPath) {
const safeFileName = escapeHTML(file.name);
const safeModified = escapeHTML(file.modified);
const safeUploaded = escapeHTML(file.uploaded);
const safeSize = escapeHTML(file.size);
const safeUploader = escapeHTML(file.uploader || "Unknown");
let previewButton = "";
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|ogg)$/i.test(file.name)) {
let previewIcon = "";
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) {
previewIcon = `<i class="material-icons">image</i>`;
} else if (/\.(mp4|webm|mov|ogg)$/i.test(file.name)) {
previewIcon = `<i class="material-icons">videocam</i>`;
} else if (/\.pdf$/i.test(file.name)) {
previewIcon = `<i class="material-icons">picture_as_pdf</i>`;
}
previewButton = `<button class="btn btn-sm btn-info preview-btn" onclick="event.stopPropagation(); previewFile('${folderPath + encodeURIComponent(file.name)}', '${safeFileName}')">
${previewIcon}
</button>`;
}
return `
<tr onclick="toggleRowSelection(event, '${safeFileName}')" class="clickable-row">
<td>
<input type="checkbox" class="file-checkbox" value="${safeFileName}" onclick="event.stopPropagation(); updateRowHighlight(this);">
</td>
<td>${safeFileName}</td>
<td class="hide-small nowrap">${safeModified}</td>
<td class="hide-small hide-medium nowrap">${safeUploaded}</td>
<td class="hide-small nowrap">${safeSize}</td>
<td class="hide-small hide-medium nowrap">${safeUploader}</td>
<td>
<div class="button-wrap" style="display: flex; justify-content: left; gap: 5px;">
<a class="btn btn-sm btn-success download-btn"
href="download.php?folder=${encodeURIComponent(file.folder || 'root')}&file=${encodeURIComponent(file.name)}"
title="Download">
<i class="material-icons">file_download</i>
</a>
${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>
` : ""}
${previewButton}
<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>
</div>
</td>
</tr>
`;
}
export function buildBottomControls(itemsPerPageSetting) {
return `
<div class="d-flex align-items-center mt-3 bottom-controls">
<label class="label-inline mr-2 mb-0">Show</label>
<select class="form-control bottom-select" onchange="changeItemsPerPage(this.value)">
${[10, 20, 50, 100].map(num => `<option value="${num}" ${num === itemsPerPageSetting ? "selected" : ""}>${num}</option>`).join("")}
</select>
<span class="items-per-page-text ml-2 mb-0">items per page</span>
</div>
`;
}
// --- Global Helper Functions ---
export function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
export function updateRowHighlight(checkbox) {
const row = checkbox.closest('tr');
if (!row) return;
if (checkbox.checked) {
row.classList.add('row-selected');
} else {
row.classList.remove('row-selected');
}
}
export function toggleRowSelection(event, fileName) {
const targetTag = event.target.tagName.toLowerCase();
if (targetTag === 'a' || targetTag === 'button' || targetTag === 'input') {
return;
}
const row = event.currentTarget;
const checkbox = row.querySelector('.file-checkbox');
if (!checkbox) return;
checkbox.checked = !checkbox.checked;
updateRowHighlight(checkbox);
updateFileActionButtons();
}
export function previewFile(fileUrl, fileName) {
let modal = document.getElementById("filePreviewModal");
if (!modal) {
modal = document.createElement("div");
modal.id = "filePreviewModal";
Object.assign(modal.style, {
display: "none",
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">
<span id="closeFileModal" class="close-image-modal">&times;</span>
<h4 class="image-modal-header"></h4>
<div class="file-preview-container"></div>
</div>`;
document.body.appendChild(modal);
document.getElementById("closeFileModal").addEventListener("click", function () {
const video = modal.querySelector("video");
if (video) {
video.pause();
video.currentTime = 0;
}
modal.style.display = "none";
});
modal.addEventListener("click", function (e) {
if (e.target === modal) {
const video = modal.querySelector("video");
if (video) {
video.pause();
video.currentTime = 0;
}
modal.style.display = "none";
}
});
}
modal.querySelector("h4").textContent = fileName;
const container = modal.querySelector(".file-preview-container");
container.innerHTML = "";
const extension = fileName.split('.').pop().toLowerCase();
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(fileName)) {
const img = document.createElement("img");
img.src = fileUrl;
img.className = "image-modal-img";
container.appendChild(img);
} 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|webm|mov|ogg)$/i.test(fileName)) {
const video = document.createElement("video");
video.src = fileUrl;
video.controls = true;
video.className = "image-modal-img";
container.appendChild(video);
} else {
container.textContent = "Preview not available for this file type.";
}
modal.style.display = "flex";
}

View File

@@ -1,59 +0,0 @@
<?php
require_once 'config.php';
// For GET requests (which download.php will use), we assume session authentication is enough.
// 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;
}
// Determine the directory.
if ($folder !== 'root') {
$directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
} else {
$directory = UPLOAD_DIR;
}
$filePath = $directory . $file;
if (!file_exists($filePath)) {
http_response_code(404);
echo json_encode(["error" => "File not found."]);
exit;
}
// Serve the file.
$mimeType = mime_content_type($filePath);
header("Content-Type: " . $mimeType);
// For images, serve inline; for other types, force download.
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
header('Content-Disposition: inline; filename="' . basename($filePath) . '"');
} else {
header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
}
header('Content-Length: ' . filesize($filePath));
// Disable caching.
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
readfile($filePath);
exit;
?>

View File

@@ -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;
?>

File diff suppressed because it is too large Load Diff

View File

@@ -1,487 +0,0 @@
// folderManager.js
import { loadFileList } from './fileManager.js';
import { showToast, escapeHTML } from './domUtils.js';
// ----------------------
// Helper Functions (Data/State)
// ----------------------
// Formats a folder name for display (e.g. adding indentations).
export function formatFolderName(folder) {
if (typeof folder !== "string") return "";
if (folder.indexOf("/") !== -1) {
let parts = folder.split("/");
let indent = "";
for (let i = 1; i < parts.length; i++) {
indent += "\u00A0\u00A0\u00A0\u00A0"; // 4 non-breaking spaces per level
}
return indent + parts[parts.length - 1];
} else {
return folder;
}
}
// Build a tree structure from a flat array of folder paths.
function buildFolderTree(folders) {
const tree = {};
folders.forEach(folderPath => {
// Ensure folderPath is a string
if (typeof folderPath !== "string") return;
const parts = folderPath.split('/');
let current = tree;
parts.forEach(part => {
if (!current[part]) {
current[part] = {};
}
current = current[part];
});
});
return tree;
}
// ----------------------
// Folder Tree State (Save/Load)
// ----------------------
function loadFolderTreeState() {
const state = localStorage.getItem("folderTreeState");
return state ? JSON.parse(state) : {};
}
function saveFolderTreeState(state) {
localStorage.setItem("folderTreeState", JSON.stringify(state));
}
// Helper for getting the parent folder.
function getParentFolder(folder) {
if (folder === "root") return "root";
const lastSlash = folder.lastIndexOf("/");
return lastSlash === -1 ? "root" : folder.substring(0, lastSlash);
}
// ----------------------
// DOM Building Functions
// ----------------------
// Recursively builds HTML for the folder tree as nested <ul> elements.
function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
const state = loadFolderTreeState();
let html = `<ul class="folder-tree ${defaultDisplay === 'none' ? 'collapsed' : 'expanded'}">`;
for (const folder in tree) {
// Skip the trash folder (case-insensitive)
if (folder.toLowerCase() === "trash") {
continue;
}
const fullPath = parentPath ? parentPath + "/" + folder : folder;
const hasChildren = Object.keys(tree[folder]).length > 0;
const displayState = state[fullPath] !== undefined ? state[fullPath] : defaultDisplay;
html += `<li class="folder-item">`;
if (hasChildren) {
const toggleSymbol = (displayState === "none") ? "[+]" : "[-]";
html += `<span class="folder-toggle" data-folder="${fullPath}">${toggleSymbol}</span>`;
} else {
html += `<span class="folder-indent-placeholder"></span>`;
}
// Use escapeHTML to safely render the folder name.
html += `<span class="folder-option" data-folder="${fullPath}">${escapeHTML(folder)}</span>`;
if (hasChildren) {
html += renderFolderTree(tree[folder], fullPath, displayState);
}
html += `</li>`;
}
html += `</ul>`;
return html;
}
// Expands the folder tree along a given path.
function expandTreePath(path) {
const parts = path.split("/");
let cumulative = "";
parts.forEach((part, index) => {
cumulative = index === 0 ? part : cumulative + "/" + part;
const option = document.querySelector(`.folder-option[data-folder="${cumulative}"]`);
if (option) {
const li = option.parentNode;
const nestedUl = li.querySelector("ul");
if (nestedUl && (nestedUl.classList.contains("collapsed") || !nestedUl.classList.contains("expanded"))) {
nestedUl.classList.remove("collapsed");
nestedUl.classList.add("expanded");
const toggle = li.querySelector(".folder-toggle");
if (toggle) {
toggle.textContent = "[-]";
let state = loadFolderTreeState();
state[cumulative] = "block";
saveFolderTreeState(state);
}
}
}
});
}
// ----------------------
// Drag & Drop Support for Folder Tree Nodes
// ----------------------
// When a draggable file is dragged over a folder node, allow the drop and add a visual cue.
function folderDragOverHandler(event) {
event.preventDefault();
event.currentTarget.classList.add("drop-hover");
}
// Remove the visual cue when the drag leaves.
function folderDragLeaveHandler(event) {
event.currentTarget.classList.remove("drop-hover");
}
// When a file is dropped onto a folder node, send a move request.
function folderDropHandler(event) {
event.preventDefault();
event.currentTarget.classList.remove("drop-hover");
const dropFolder = event.currentTarget.getAttribute("data-folder");
let dragData;
try {
dragData = JSON.parse(event.dataTransfer.getData("application/json"));
} catch (e) {
console.error("Invalid drag data");
return;
}
// Use the files array if present, or fall back to a single file.
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
if (filesToMove.length === 0) return;
fetch("moveFiles.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content")
},
body: JSON.stringify({
source: dragData.sourceFolder,
files: filesToMove,
destination: dropFolder
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast(`File(s) moved successfully to ${dropFolder}!`);
loadFileList(dragData.sourceFolder);
} else {
showToast("Error moving files: " + (data.error || "Unknown error"));
}
})
.catch(error => {
console.error("Error moving files via drop:", error);
showToast("Error moving files.");
});
}
// ----------------------
// Main Folder Tree Rendering and Event Binding
// ----------------------
export async function loadFolderTree(selectedFolder) {
try {
const response = await fetch('getFolderList.php');
if (response.status === 401) {
console.error("Unauthorized: Please log in to view folders.");
showToast("Session expired. Please log in again.");
window.location.href = "logout.php";
return;
}
let folders = await response.json();
// If returned items are objects (with a "folder" property), extract folder paths.
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
folders = folders.map(item => item.folder);
}
// Filter out duplicate "root" entries if present.
folders = folders.filter(folder => folder !== "root");
if (!Array.isArray(folders)) {
console.error("Folder list response is not an array:", folders);
return;
}
const container = document.getElementById("folderTreeContainer");
if (!container) {
console.error("Folder tree container not found.");
return;
}
let html = `<div id="rootRow" class="root-row">
<span class="folder-toggle" data-folder="root">[-]</span>
<span class="folder-option root-folder-option" data-folder="root">(Root)</span>
</div>`;
if (folders.length === 0) {
html += `<ul class="folder-tree expanded">
<li class="folder-item">
<span class="folder-option" data-folder="root">(Root)</span>
</li>
</ul>`;
} else {
const tree = buildFolderTree(folders);
html += renderFolderTree(tree, "", "block");
}
container.innerHTML = html;
// Attach drag-and-drop event listeners to folder nodes.
container.querySelectorAll(".folder-option").forEach(el => {
el.addEventListener("dragover", folderDragOverHandler);
el.addEventListener("dragleave", folderDragLeaveHandler);
el.addEventListener("drop", folderDropHandler);
});
// Determine current folder.
if (selectedFolder) {
window.currentFolder = selectedFolder;
} else {
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
}
localStorage.setItem("lastOpenedFolder", window.currentFolder);
document.getElementById("fileListTitle").textContent =
window.currentFolder === "root" ? "Files in (Root)" : "Files in (" + window.currentFolder + ")";
loadFileList(window.currentFolder);
// Expand tree to current folder.
const folderState = loadFolderTreeState();
if (window.currentFolder !== "root" && folderState[window.currentFolder] !== "none") {
expandTreePath(window.currentFolder);
}
// Highlight current folder.
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
if (selectedEl) {
selectedEl.classList.add("selected");
}
// Event binding for folder selection.
container.querySelectorAll(".folder-option").forEach(el => {
el.addEventListener("click", function (e) {
e.stopPropagation();
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
this.classList.add("selected");
const selected = this.getAttribute("data-folder");
window.currentFolder = selected;
localStorage.setItem("lastOpenedFolder", selected);
document.getElementById("fileListTitle").textContent =
selected === "root" ? "Files in (Root)" : "Files in (" + selected + ")";
loadFileList(selected);
});
});
// Event binding for toggling folders.
const rootToggle = container.querySelector("#rootRow .folder-toggle");
if (rootToggle) {
rootToggle.addEventListener("click", function (e) {
e.stopPropagation();
const nestedUl = container.querySelector("#rootRow + ul");
if (nestedUl) {
let state = loadFolderTreeState();
if (nestedUl.classList.contains("collapsed") || !nestedUl.classList.contains("expanded")) {
nestedUl.classList.remove("collapsed");
nestedUl.classList.add("expanded");
this.textContent = "[-]";
state["root"] = "block";
} else {
nestedUl.classList.remove("expanded");
nestedUl.classList.add("collapsed");
this.textContent = "[+]";
state["root"] = "none";
}
saveFolderTreeState(state);
}
});
}
container.querySelectorAll(".folder-toggle").forEach(toggle => {
toggle.addEventListener("click", function (e) {
e.stopPropagation();
const siblingUl = this.parentNode.querySelector("ul");
const folderPath = this.getAttribute("data-folder");
let state = loadFolderTreeState();
if (siblingUl) {
if (siblingUl.classList.contains("collapsed") || !siblingUl.classList.contains("expanded")) {
siblingUl.classList.remove("collapsed");
siblingUl.classList.add("expanded");
this.textContent = "[-]";
state[folderPath] = "block";
} else {
siblingUl.classList.remove("expanded");
siblingUl.classList.add("collapsed");
this.textContent = "[+]";
state[folderPath] = "none";
}
saveFolderTreeState(state);
}
});
});
} catch (error) {
console.error("Error loading folder tree:", error);
}
}
// For backward compatibility.
export function loadFolderList(selectedFolder) {
loadFolderTree(selectedFolder);
}
// ----------------------
// Folder Management (Rename, Delete, Create)
// ----------------------
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
function openRenameFolderModal() {
const selectedFolder = window.currentFolder || "root";
if (!selectedFolder || selectedFolder === "root") {
showToast("Please select a valid folder to rename.");
return;
}
const parts = selectedFolder.split("/");
document.getElementById("newRenameFolderName").value = parts[parts.length - 1];
document.getElementById("renameFolderModal").style.display = "block";
}
document.getElementById("cancelRenameFolder").addEventListener("click", function () {
document.getElementById("renameFolderModal").style.display = "none";
document.getElementById("newRenameFolderName").value = "";
});
document.getElementById("submitRenameFolder").addEventListener("click", function (event) {
event.preventDefault();
const selectedFolder = window.currentFolder || "root";
const newNameBasename = document.getElementById("newRenameFolderName").value.trim();
if (!newNameBasename || newNameBasename === selectedFolder.split("/").pop()) {
showToast("Please enter a valid new folder name.");
return;
}
const parentPath = getParentFolder(selectedFolder);
const newFolderFull = parentPath === "root" ? newNameBasename : parentPath + "/" + newNameBasename;
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
if (!csrfToken) {
showToast("CSRF token not loaded yet! Please try again.");
return;
}
fetch("renameFolder.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken
},
body: JSON.stringify({ oldFolder: window.currentFolder, newFolder: newFolderFull })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("Folder renamed successfully!");
window.currentFolder = newFolderFull;
localStorage.setItem("lastOpenedFolder", newFolderFull);
loadFolderList(newFolderFull);
} else {
showToast("Error: " + (data.error || "Could not rename folder"));
}
})
.catch(error => console.error("Error renaming folder:", error))
.finally(() => {
document.getElementById("renameFolderModal").style.display = "none";
document.getElementById("newRenameFolderName").value = "";
});
});
function openDeleteFolderModal() {
const selectedFolder = window.currentFolder || "root";
if (!selectedFolder || selectedFolder === "root") {
showToast("Please select a valid folder to delete.");
return;
}
document.getElementById("deleteFolderMessage").textContent =
"Are you sure you want to delete folder " + selectedFolder + "?";
document.getElementById("deleteFolderModal").style.display = "block";
}
document.getElementById("cancelDeleteFolder").addEventListener("click", function () {
document.getElementById("deleteFolderModal").style.display = "none";
});
document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
const selectedFolder = window.currentFolder || "root";
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch("deleteFolder.php", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken
},
body: JSON.stringify({ folder: selectedFolder })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("Folder deleted successfully!");
window.currentFolder = getParentFolder(selectedFolder);
localStorage.setItem("lastOpenedFolder", window.currentFolder);
loadFolderList(window.currentFolder);
} else {
showToast("Error: " + (data.error || "Could not delete folder"));
}
})
.catch(error => console.error("Error deleting folder:", error))
.finally(() => {
document.getElementById("deleteFolderModal").style.display = "none";
});
});
document.getElementById("createFolderBtn").addEventListener("click", function () {
document.getElementById("createFolderModal").style.display = "block";
});
document.getElementById("cancelCreateFolder").addEventListener("click", function () {
document.getElementById("createFolderModal").style.display = "none";
document.getElementById("newFolderName").value = "";
});
document.getElementById("submitCreateFolder").addEventListener("click", function () {
const folderInput = document.getElementById("newFolderName").value.trim();
if (!folderInput) {
showToast("Please enter a folder name.");
return;
}
let selectedFolder = window.currentFolder || "root";
let fullFolderName = folderInput;
if (selectedFolder && selectedFolder !== "root") {
fullFolderName = selectedFolder + "/" + folderInput;
}
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch("createFolder.php", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken
},
body: JSON.stringify({
folderName: folderInput,
parent: selectedFolder === "root" ? "" : selectedFolder
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("Folder created successfully!");
window.currentFolder = fullFolderName;
localStorage.setItem("lastOpenedFolder", fullFolderName);
loadFolderList(fullFolderName);
} else {
showToast("Error: " + (data.error || "Could not create folder"));
}
document.getElementById("createFolderModal").style.display = "none";
document.getElementById("newFolderName").value = "";
})
.catch(error => {
console.error("Error creating folder:", error);
document.getElementById("createFolderModal").style.display = "none";
});
});

View File

@@ -1,101 +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
];
}
echo json_encode(["files" => $fileList]);
?>

View File

@@ -1,97 +0,0 @@
<?php
require '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);
?>

View File

@@ -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;
?>

View File

@@ -1,24 +0,0 @@
<?php
require '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) {
// Optionally, validate username format:
if (preg_match('/^[A-Za-z0-9_\- ]+$/', $parts[0])) {
$users[] = ["username" => $parts[0]];
}
}
}
}
echo json_encode($users);
?>

View File

@@ -1,374 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Multi File Upload Editor</title>
<link rel="icon" type="image/png" href="/assets/logo.png">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<meta name="csrf-token" content="">
<meta name="share-url" content="">
<!-- Google Fonts and Material Icons -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/xml/xml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/css/css.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/javascript/javascript.min.js"></script>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<header class="header-container">
<div class="header-left">
<div class="header-logo">
<svg version="1.1" id="filingCabinetLogo" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64" xml:space="preserve">
<defs>
<!-- Gradient for the cabinet body -->
<linearGradient id="cabinetGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
<!-- Drop shadow filter -->
<filter id="shadowFilter" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2" />
</filter>
</defs>
<style type="text/css">
/* Cabinet with gradient, white outline, and drop shadow */
.cabinet {
fill: url(#cabinetGradient);
stroke: white;
stroke-width: 2;
}
.divider {
stroke: #1565C0;
stroke-width: 1.5;
}
.drawer {
fill: #FFFFFF;
}
.handle {
fill: #1565C0;
}
</style>
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
<!-- Divider lines for drawers -->
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
<!-- Drawers with Handles -->
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="27" r="1.5" class="handle" />
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="39" r="1.5" class="handle" />
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="51" r="1.5" class="handle" />
<!-- Additional detail: a small top handle on the cabinet door -->
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
</svg>
</div>
</div>
<div class="header-title">
<h1>Multi File Upload Editor</h1>
</div>
<div class="header-right">
<div class="header-buttons">
<button id="logoutBtn" title="Logout">
<i class="material-icons">exit_to_app</i>
</button>
<!-- Restore Files Modal (Admin Only) -->
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
<div class="modal-content">
<h4 class="custom-restore-header">
<i class="material-icons orange-icon">restore_from_trash</i>
<span>Restore or</span>
<i class="material-icons red-icon">delete_for_ever</i>
<span>Delete Trash Items</span>
</h4>
<div id="restoreFilesList"
style="max-height:300px; overflow-y:auto; border:1px solid #ccc; padding:10px; margin-bottom:10px;">
<!-- Trash items will be loaded here -->
</div>
<div style="text-align: right;">
<button id="restoreSelectedBtn" class="btn btn-primary">Restore Selected</button>
<button id="restoreAllBtn" class="btn btn-secondary">Restore All</button>
<button id="deleteTrashSelectedBtn" class="btn btn-warning">Delete Selected</button>
<button id="deleteAllBtn" class="btn btn-danger">Delete All</button>
<button id="closeRestoreModal" class="btn btn-dark">Close</button>
</div>
</div>
</div>
<button id="addUserBtn" title="Add User">
<i class="material-icons">person_add</i>
</button>
<button id="removeUserBtn" title="Remove User">
<i class="material-icons">person_remove</i>
</button>
<button id="darkModeToggle" class="dark-mode-toggle">Dark Mode</button>
</div>
</div>
</header>
<!-- Custom Toast Container -->
<div id="customToast"></div>
<div class="container-fluid">
<!-- Login Form -->
<div class="row" id="loginForm">
<div class="col-12">
<form id="authForm" method="post">
<div class="form-group">
<label for="loginUsername">User:</label>
<input type="text" class="form-control" id="loginUsername" name="username" required />
</div>
<div class="form-group">
<label for="loginPassword">Password:</label>
<input type="password" class="form-control" id="loginPassword" name="password" required />
</div>
<button type="submit" class="btn btn-primary btn-block btn-login">Login</button>
</form>
</div>
</div>
<!-- Main Operations: Upload and Folder Management -->
<div id="mainOperations">
<div class="container" style="max-width: 1400px; margin: 0 auto;">
<div class="row align-items-start" id="uploadFolderRow">
<!-- Upload Card: 50% width on medium, 58% on large -->
<div class="col-md-6 col-lg-7 d-flex">
<div id="uploadCard" class="card flex-fill" style="max-width: 900px; width: 100%;">
<div class="card-header">Upload Files/Folders</div>
<div class="card-body d-flex flex-column">
<form id="uploadFileForm" method="post" enctype="multipart/form-data" class="d-flex flex-column"
style="height: 100%;">
<div class="form-group flex-grow-1" style="margin-bottom: 1rem;">
<div id="uploadDropArea"
style="border:2px dashed #ccc; padding:20px; cursor:pointer; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center;">
<span>Drop files/folders here or click 'Choose files'</span>
<br />
<input type="file" id="file" name="file[]" class="form-control-file" multiple required
webkitdirectory directory mozdirectory style="opacity:0; position:absolute; z-index:-1;" />
<button type="button" onclick="document.getElementById('file').click();">Choose Folder</button>
</div>
</div>
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto">Upload</button>
<div id="uploadProgressContainer"></div>
</form>
</div>
</div>
</div>
<!-- Folder Management Card -->
<div class="col-md-6 col-lg-5 d-flex">
<div id="folderManagementCard" class="card flex-fill" style="max-width: 900px; width: 100%; position: relative;">
<!-- Card header with folder management title and help icon -->
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
<span>Folder Navigation &amp; Management</span>
<button id="folderHelpBtn" class="btn btn-link" title="Folder Help"
style="padding: 0; border: none; background: none;">
<i class="material-icons folder-help-icon" style="font-size: 24px;">info</i>
</button>
</div>
<div class="card-body custom-folder-card-body">
<div class="form-group d-flex align-items-top" style="padding-top:0; margin-bottom:0;">
<div id="folderTreeContainer"></div>
</div>
<!-- Folder actions (create, rename, delete) -->
<div class="folder-actions mt-3">
<button id="createFolderBtn" class="btn btn-primary">Create Folder</button>
<!-- Create Folder Modal -->
<div id="createFolderModal" class="modal">
<div class="modal-content">
<h4>Create Folder</h4>
<input type="text" id="newFolderName" class="form-control" placeholder="Enter folder name"
style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelCreateFolder" class="btn btn-secondary">Cancel</button>
<button id="submitCreateFolder" class="btn btn-primary">Create</button>
</div>
</div>
</div>
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder">
<i class="material-icons">drive_file_rename_outline</i>
</button>
<!-- Rename Folder Modal -->
<div id="renameFolderModal" class="modal">
<div class="modal-content">
<h4>Rename Folder</h4>
<input type="text" id="newRenameFolderName" class="form-control"
placeholder="Enter new folder name" style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFolder" class="btn btn-secondary">Cancel</button>
<button id="submitRenameFolder" class="btn btn-primary">Rename</button>
</div>
</div>
</div>
<button id="deleteFolderBtn" class="btn btn-danger ml-2" title="Delete Folder">
<i class="material-icons">delete</i>
</button>
<!-- Delete Folder Modal -->
<div id="deleteFolderModal" class="modal">
<div class="modal-content">
<h4>Delete Folder</h4>
<p id="deleteFolderMessage">Are you sure you want to delete this folder?</p>
<div style="margin-top:15px; text-align:right;">
<button id="cancelDeleteFolder" class="btn btn-secondary">Cancel</button>
<button id="confirmDeleteFolder" class="btn btn-danger">Delete</button>
</div>
</div>
</div>
</div>
<!-- Help Tooltip: Initially hidden -->
<div id="folderHelpTooltip" class="folder-help-tooltip"
style="display: none; position: absolute; top: 50px; right: 15px; background: #fff; border: 1px solid #ccc; padding: 10px; z-index: 1000; box-shadow: 2px 2px 6px rgba(0,0,0,0.2);">
<ul class="folder-help-list" style="margin: 0; padding-left: 20px;">
<li>Click on a folder in the tree to view its files.</li>
<li>Use [-] to collapse and [+] to expand folders.</li>
<li>Select a folder and click "Create Folder" to add a subfolder.</li>
<li>To rename or delete a folder, select it and then click the appropriate button.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- File List Section -->
<div id="fileListContainer" style="display: none;">
<h2 id="fileListTitle">Files in (Root)</h2>
<div id="fileListActions" class="file-list-actions">
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;">Delete Files</button>
<!-- Delete Files Modal -->
<div id="deleteFilesModal" class="modal">
<div class="modal-content">
<h4>Delete Selected Files</h4>
<p id="deleteFilesMessage">Are you sure you want to delete the selected files?</p>
<div class="modal-footer">
<button id="cancelDeleteFiles" class="btn btn-secondary">Cancel</button>
<button id="confirmDeleteFiles" class="btn btn-danger">Delete</button>
</div>
</div>
</div>
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled>Copy Files</button>
<!-- Copy Files Modal -->
<div id="copyFilesModal" class="modal">
<div class="modal-content">
<h4>Copy Selected Files</h4>
<p id="copyFilesMessage">Select a target folder for copying the selected files:</p>
<select id="copyTargetFolder" class="form-control modal-input"></select>
<div class="modal-footer">
<button id="cancelCopyFiles" class="btn btn-secondary">Cancel</button>
<button id="confirmCopyFiles" class="btn btn-primary">Copy</button>
</div>
</div>
</div>
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled>Move Files</button>
<!-- Move Files Modal -->
<div id="moveFilesModal" class="modal">
<div class="modal-content">
<h4>Move Selected Files</h4>
<p id="moveFilesMessage">Select a target folder for moving the selected files:</p>
<select id="moveTargetFolder" class="form-control modal-input"></select>
<div class="modal-footer">
<button id="cancelMoveFiles" class="btn btn-secondary">Cancel</button>
<button id="confirmMoveFiles" class="btn btn-primary">Move</button>
</div>
</div>
</div>
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled>Download ZIP</button>
<!-- Download Zip Modal -->
<div id="downloadZipModal" class="modal" style="display:none;">
<div class="modal-content">
<h4>Download Selected Files as Zip</h4>
<p>Enter a name for the zip file:</p>
<input type="text" id="zipFileNameInput" class="form-control" placeholder="files.zip" />
<div class="modal-footer" style="margin-top:15px; text-align:right;">
<button id="cancelDownloadZip" class="btn btn-secondary">Cancel</button>
<button id="confirmDownloadZip" class="btn btn-primary">Download</button>
</div>
</div>
</div>
</div>
<div id="fileList"></div>
</div>
</div>
<!-- Add User Modal -->
<div id="addUserModal" class="modal">
<div class="modal-content">
<h3>Create New User</h3>
<label for="newUsername">Username:</label>
<input type="text" id="newUsername" class="form-control" />
<label for="newPassword">Password:</label>
<input type="password" id="newPassword" class="form-control" />
<div id="adminCheckboxContainer">
<input type="checkbox" id="isAdmin" />
<label for="isAdmin">Grant Admin Access</label>
</div>
<div class="button-container">
<button id="cancelUserBtn" class="btn btn-secondary">Cancel</button>
<button id="saveUserBtn" class="btn btn-primary">Save User</button>
</div>
</div>
</div>
<!-- Remove User Modal -->
<div id="removeUserModal" class="modal">
<div class="modal-content">
<h3>Remove User</h3>
<label for="removeUsernameSelect">Select a user to remove:</label>
<select id="removeUsernameSelect" class="form-control"></select>
<div class="button-container">
<button id="cancelRemoveUserBtn" class="btn btn-secondary">Cancel</button>
<button id="deleteUserBtn" class="btn btn-danger">Delete User</button>
</div>
</div>
</div>
<!-- Rename File Modal -->
<div id="renameFileModal" class="modal">
<div class="modal-content">
<h4>Rename File</h4>
<input type="text" id="newFileName" class="form-control" placeholder="Enter new file name"
style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFile" class="btn btn-secondary">Cancel</button>
<button id="submitRenameFile" class="btn btn-primary">Rename</button>
</div>
</div>
</div>
<!-- Custom Confirm Modal -->
<div id="customConfirmModal" class="modal" style="display:none;">
<div class="modal-content">
<p id="confirmMessage"></p>
<div class="modal-actions">
<button id="confirmYesBtn" class="btn btn-primary">Yes</button>
<button id="confirmNoBtn" class="btn btn-secondary">No</button>
</div>
</div>
</div>
<!-- JavaScript Files -->
<script type="module" src="main.js"></script>
</body>
</html>

View File

@@ -1,19 +0,0 @@
<?php
session_start();
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
// Fallback: If a CSRF token exists in the session and doesn't match the one provided,
// log the mismatch but proceed with logout.
if (isset($_SESSION['csrf_token']) && $receivedToken !== $_SESSION['csrf_token']) {
// Optionally log this event:
error_log("CSRF token mismatch on logout. Proceeding with logout.");
}
$_SESSION = []; // Clear session data
session_destroy(); // Destroy session
header('Content-Type: application/json');
echo json_encode(["success" => "Logged out"]);
exit;
?>

155
main.js
View File

@@ -1,155 +0,0 @@
import { sendRequest } from './networkUtils.js';
import {
toggleVisibility,
toggleAllCheckboxes,
updateFileActionButtons,
showToast
} from './domUtils.js';
import {
loadFileList,
initFileActions,
editFile,
saveFile,
displayFilePreview,
renameFile
} from './fileManager.js';
import { loadFolderTree } from './folderManager.js';
import { initUpload } from './upload.js';
import { initAuth, checkAuthentication } from './auth.js';
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
function loadCsrfToken() {
fetch('token.php', { credentials: 'include' })
.then(response => 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);
})
.catch(error => console.error("Error loading CSRF token and share URL:", error));
}
document.addEventListener("DOMContentLoaded", loadCsrfToken);
// Expose functions for inline handlers.
window.sendRequest = sendRequest;
window.toggleVisibility = toggleVisibility;
window.toggleAllCheckboxes = toggleAllCheckboxes;
window.editFile = editFile;
window.saveFile = saveFile;
window.renameFile = renameFile;
// Global variable for the current folder.
window.currentFolder = "root";
document.addEventListener("DOMContentLoaded", function () {
// Call initAuth synchronously.
initAuth();
// --- 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 = "Light Mode";
} else {
document.body.classList.remove("dark-mode");
if (darkModeToggle) darkModeToggle.textContent = "Dark Mode";
}
});
}
// --- End Dark Mode Persistence ---
const message = sessionStorage.getItem("welcomeMessage");
if (message) {
showToast(message);
sessionStorage.removeItem("welcomeMessage");
}
checkAuthentication().then(authenticated => {
if (authenticated) {
window.currentFolder = "root";
loadFileList(window.currentFolder);
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.");
}
});
// --- 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);
}
});
});

View File

@@ -1 +0,0 @@
[]

View File

@@ -1,158 +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;
}
$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)]);
}
?>

View File

@@ -1,32 +0,0 @@
// networkUtils.js
export function sendRequest(url, method = "GET", data = null) {
console.log("Sending request to:", url, "with method:", method);
const options = {
method,
credentials: 'include', // include cookies in requests
headers: {}
};
// If data is provided and is not FormData, assume JSON.
if (data && !(data instanceof FormData)) {
options.headers["Content-Type"] = "application/json";
options.body = JSON.stringify(data);
} else if (data instanceof FormData) {
// For FormData, don't set the Content-Type header; the browser will handle it.
options.body = data;
}
return fetch(url, options)
.then(response => {
console.log("Response status:", response.status);
if (!response.ok) {
return response.text().then(text => {
throw new Error(`HTTP error ${response.status}: ${text}`);
});
}
return response.json().catch(() => {
console.warn("Response is not JSON, returning as text");
return response.text();
});
});
}

2599
openapi.json.dist Normal file

File diff suppressed because it is too large Load Diff

75
public/.htaccess Normal file
View File

@@ -0,0 +1,75 @@
# -----------------------------
# 1) Prevent directory listings
# -----------------------------
Options -Indexes
# -----------------------------
# Default index files
# -----------------------------
DirectoryIndex index.html
# -----------------------------
# Deny access to hidden files
# -----------------------------
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# -----------------------------
# Enforce HTTPS (optional)
# -----------------------------
RewriteEngine On
#RewriteCond %{HTTPS} off
#RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
<IfModule mod_headers.c>
# Allow requests from a specific origin
#Header set Access-Control-Allow-Origin "https://demo.filerise.net"
Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, X-CSRF-Token"
Header set Access-Control-Allow-Credentials "true"
</IfModule>
<IfModule mod_headers.c>
# Prevent clickjacking
Header always set X-Frame-Options "SAMEORIGIN"
# Block XSS
Header always set X-XSS-Protection "1; mode=block"
# No MIME sniffing
Header always set X-Content-Type-Options "nosniff"
</IfModule>
<IfModule mod_headers.c>
# HTML: always revalidate
<FilesMatch "\.(html|htm)$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
</FilesMatch>
# JS/CSS: shortterm cache, revalidate regularly
<FilesMatch "\.(js|css)$">
Header set Cache-Control "public, max-age=3600, must-revalidate"
</FilesMatch>
</IfModule>
# -----------------------------
# Additional Security Headers
# -----------------------------
<IfModule mod_headers.c>
# Enforce HTTPS for a year with subdomains and preload option.
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Set a Referrer Policy.
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Permissions Policy: disable features you don't need.
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
# IE-specific header to prevent downloads from opening in IE.
Header always set X-Download-Options "noopen"
# Expect-CT header for Certificate Transparency (optional).
Header always set Expect-CT "max-age=86400, enforce"
</IfModule>
# -----------------------------
# Disable TRACE method
# -----------------------------
RewriteCond %{REQUEST_METHOD} ^TRACE
RewriteRule .* - [F]

31
public/api.php Normal file
View 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
View 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();

View 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();

View 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;

View 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
View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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
View 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

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because it is too large Load Diff

521
public/index.html Normal file
View File

@@ -0,0 +1,521 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title data-i18n-key="title">FileRise</title>
<link rel="icon" type="image/png" href="/assets/logo.png">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<meta name="csrf-token" content="">
<meta name="share-url" content="">
<style>
/* hide the app shell until JS says otherwise */
.main-wrapper { display: none; }
/* full-screen white overlay while we check auth */
#loadingOverlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-color,#fff);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
</style>
<!-- Google Fonts and Material Icons -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css"
integrity="sha384-zaeBlB/vwYsDRSlFajnDd7OydJ0cWk+c2OWybl3eSUf6hW2EbhlCsQPqKr3gkznT" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.css"
integrity="sha384-eZTPTN0EvJdn23s24UDYJmUM2T7C2ZFa3qFLypeBruJv8mZeTusKUAO/j5zPAQ6l" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"
integrity="sha384-UXbkZAbZYZ/KCAslc6UO4d6UHNKsOxZ/sqROSQaPTZCuEIKhfbhmffQ64uXFOcma"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/xml/xml.min.js"
integrity="sha384-xPpkMo5nDgD98fIcuRVYhxkZV6/9Y4L8s3p0J5c4MxgJkyKJ8BJr+xfRkq7kn6Tw"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/css/css.min.js"
integrity="sha384-to8njsu2GAiXQnY/aLGzz0DIY/SFSeSDodtvSl869n2NmsBdHOTZNNqbEBPYh7Pa"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/javascript/javascript.min.js"
integrity="sha384-kmQrbJf09Uo1WRLMDVGoVG3nM6F48frIhcj7f3FDUjeRzsiHwyBWDjMUIttnIeAf"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/resumable.js/1.1.0/resumable.min.js"
integrity="sha384-EXTg7rRfdTPZWoKVCslusAAev2TYw76fm+Wox718iEtFQ+gdAdAc5Z/ndLHSo4mq"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min.js"
integrity="sha384-zPE55eyESN+FxCWGEnlNxGyAPJud6IZ6TtJmXb56OFRGhxZPN4akj9rjA3gw5Qqa"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="css/styles.css" />
</head>
<body>
<header class="header-container">
<div class="header-left">
<a href="index.html">
<div class="header-logo">
<svg version="1.1" id="filingCabinetLogo" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64" xml:space="preserve">
<defs>
<!-- Gradient for the cabinet body -->
<linearGradient id="cabinetGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
<!-- Drop shadow filter with animated attributes for a lifting effect -->
<filter id="shadowFilter" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow id="dropShadow" dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2">
<!-- Animate the vertical offset: from 2 to 1 (as it rises), hold, then back to 2 -->
<animate attributeName="dy" values="2;1;1;2" keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
<!-- Animate the blur similarly: from 2 to 1.5 then back to 2 -->
<animate attributeName="stdDeviation" values="2;1.5;1.5;2" keyTimes="0;0.2;0.8;1" dur="5s"
fill="freeze" />
</feDropShadow>
</filter>
</defs>
<style type="text/css">
/* Cabinet with gradient, white outline, and drop shadow */
.cabinet {
fill: url(#cabinetGradient);
stroke: white;
stroke-width: 2;
}
.divider {
stroke: #1565C0;
stroke-width: 1.5;
}
.drawer {
fill: #FFFFFF;
}
.handle {
fill: #1565C0;
}
</style>
<!-- Group that will animate upward and then back down once -->
<g id="cabinetGroup">
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
<!-- Divider lines for drawers -->
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
<!-- Drawers with Handles -->
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="27" r="1.5" class="handle" />
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="39" r="1.5" class="handle" />
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="51" r="1.5" class="handle" />
<!-- Additional detail: a small top handle on the cabinet door -->
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
<!-- Animate transform: rises by 2 pixels over 1s, holds for 3s, then falls over 1s (total 5s) -->
<animateTransform attributeName="transform" type="translate" values="0 0; 0 -2; 0 -2; 0 0"
keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
</g>
</svg>
</div>
</a>
</div>
<div class="header-title">
<h1 data-i18n-key="header_title">FileRise</h1>
</div>
<div class="header-right">
<div class="header-buttons-wrapper" style="display: flex; align-items: center; gap: 10px;">
<!-- Your header drop zone -->
<div id="headerDropArea" class="header-drop-zone"></div>
<div class="header-buttons">
<button id="logoutBtn" data-i18n-title="logout">
<i class="material-icons">exit_to_app</i>
</button>
<button id="changePasswordBtn" data-i18n-title="change_password" style="display: none;">
<i class="material-icons">vpn_key</i>
</button>
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
<div class="modal-content">
<h4 class="custom-restore-header">
<i class="material-icons orange-icon">restore_from_trash</i>
<span data-i18n-key="restore_text">Restore or</span>
<i class="material-icons red-icon">delete_for_ever</i>
<span data-i18n-key="delete_text">Delete Trash Items</span>
</h4>
<div id="restoreFilesList"
style="max-height:300px; overflow-y:auto; border:1px solid #ccc; padding:10px; margin-bottom:10px;">
<!-- Trash items will be loaded here -->
</div>
<div style="text-align: right;">
<button id="restoreSelectedBtn" class="btn btn-primary" data-i18n-key="restore_selected">Restore
Selected</button>
<button id="restoreAllBtn" class="btn btn-secondary" data-i18n-key="restore_all">Restore All</button>
<button id="deleteTrashSelectedBtn" class="btn btn-warning" data-i18n-key="delete_selected_trash">Delete
Selected</button>
<button id="deleteAllBtn" class="btn btn-danger" data-i18n-key="delete_all">Delete All</button>
<button id="closeRestoreModal" class="btn btn-dark" data-i18n-key="close">Close</button>
</div>
</div>
</div>
<button id="addUserBtn" data-i18n-title="add_user" style="display: none;">
<i class="material-icons">person_add</i>
</button>
<button id="removeUserBtn" data-i18n-title="remove_user" style="display: none;">
<i class="material-icons">person_remove</i>
</button>
<button id="darkModeToggle" class="btn-icon" aria-label="Toggle dark mode">
<span class="material-icons" id="darkModeIcon">
dark_mode
</span>
</button>
</div>
</div>
</div>
</header>
<div id="loadingOverlay"></div>
<!-- Custom Toast Container -->
<div id="customToast"></div>
<div id="hiddenCardsContainer" style="display:none;"></div>
<div class="row mt-4" id="loginForm">
<div class="col-12">
<form id="authForm" method="post">
<div class="form-group">
<label for="loginUsername" data-i18n-key="user">User:</label>
<input type="text" class="form-control" id="loginUsername" name="username" required autofocus />
</div>
<div class="form-group">
<label for="loginPassword" data-i18n-key="password">Password:</label>
<input type="password" class="form-control" id="loginPassword" name="password" required />
</div>
<button type="submit" class="btn btn-primary btn-block btn-login" data-i18n-key="login">Login</button>
<div class="form-group remember-me-container">
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
<label for="rememberMeCheckbox" data-i18n-key="remember_me">Remember me</label>
</div>
</form>
<!-- OIDC Login Option -->
<div class="text-center mt-3">
<button id="oidcLoginBtn" class="btn btn-secondary" data-i18n-key="login_oidc">Login with OIDC</button>
</div>
<!-- Basic HTTP Login Option -->
<div class="text-center mt-3">
<a href="/api/auth/login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic
HTTP
Login</a>
</div>
</div>
</div>
<!-- Main Wrapper: Hidden by default; remove "display: none;" after login -->
<div class="main-wrapper">
<!-- Sidebar Drop Zone: Hidden until you drag a card (display controlled by JS) -->
<div id="sidebarDropArea" class="drop-target-sidebar"></div>
<!-- Main Column -->
<div id="mainColumn" class="main-column">
<div class="container-fluid">
<!-- Main Operations: Upload and Folder Management -->
<div id="mainOperations">
<div class="container" style="max-width: 1400px; margin: 0 auto;">
<!-- Top Zone: Two columns (60% and 40%) -->
<div id="uploadFolderRow" class="row">
<!-- Left Column (60% for Upload Card) -->
<div id="leftCol" class="col-md-7" style="display: flex; justify-content: center;">
<div id="uploadCard" class="card" style="width: 100%;">
<div class="card-header" data-i18n-key="upload_header">Upload Files/Folders</div>
<div class="card-body d-flex flex-column">
<form id="uploadFileForm" method="post" enctype="multipart/form-data" class="d-flex flex-column">
<div class="form-group flex-grow-1" style="margin-bottom: 1rem;">
<div id="uploadDropArea"
style="border:2px dashed #ccc; padding:20px; cursor:pointer; display:flex; flex-direction:column; justify-content:center; align-items:center; position:relative;">
<span data-i18n-key="upload_instruction">Drop files/folders here or click 'Choose
Files'</span>
<br />
<input type="file" id="file" name="file[]" class="form-control-file" multiple
style="opacity:0; position:absolute; width:1px; height:1px;" />
<button type="button" id="customChooseBtn" data-i18n-key="choose_files">Choose Files</button>
</div>
</div>
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto"
data-i18n-key="upload">Upload</button>
<div id="uploadProgressContainer"></div>
</form>
</div>
</div>
</div>
<!-- Right Column (40% for Folder Management Card) -->
<div id="rightCol" class="col-md-5" style="display: flex; justify-content: center;">
<div id="folderManagementCard" class="card" style="width: 100%; position: relative;">
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
<span data-i18n-key="folder_navigation">Folder Navigation &amp; Management</span>
<button id="folderHelpBtn" class="btn btn-link" data-i18n-title="folder_help"
style="padding: 0; border: none; background: none;">
<i class="material-icons folder-help-icon" style="font-size: 24px;">info</i>
</button>
</div>
<div class="card-body custom-folder-card-body">
<div class="form-group d-flex align-items-top" style="padding-top:0; margin-bottom:0;">
<div id="folderTreeContainer"></div>
</div>
<div class="folder-actions mt-3">
<button id="createFolderBtn" class="btn btn-primary" data-i18n-title="create_folder">
<i class="material-icons">create_new_folder</i>
</button>
<div id="createFolderModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="create_folder_title">Create Folder</h4>
<input type="text" id="newFolderName" class="form-control"
data-i18n-placeholder="enter_folder_name" placeholder="Enter folder name"
style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelCreateFolder" class="btn btn-secondary"
data-i18n-key="cancel">Cancel</button>
<button id="submitCreateFolder" class="btn btn-primary"
data-i18n-key="create">Create</button>
</div>
</div>
</div>
<button id="renameFolderBtn" class="btn btn-warning ml-2" data-i18n-title="rename_folder">
<i class="material-icons">drive_file_rename_outline</i>
</button>
<div id="renameFolderModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="rename_folder_title">Rename Folder</h4>
<input type="text" id="newRenameFolderName" class="form-control"
data-i18n-placeholder="rename_folder_placeholder" placeholder="Enter new folder name"
style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFolder" class="btn btn-secondary"
data-i18n-key="cancel">Cancel</button>
<button id="submitRenameFolder" class="btn btn-primary"
data-i18n-key="rename">Rename</button>
</div>
</div>
</div>
<button id="shareFolderBtn" class="btn btn-secondary ml-2" data-i18n-title="share_folder">
<i class="material-icons">share</i>
</button>
<button id="deleteFolderBtn" class="btn btn-danger ml-2" data-i18n-title="delete_folder">
<i class="material-icons">delete</i>
</button>
<div id="deleteFolderModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="delete_folder_title">Delete Folder</h4>
<p id="deleteFolderMessage" data-i18n-key="delete_folder_message">Are you sure you want to
delete this folder?</p>
<div style="margin-top:15px; text-align:right;">
<button id="cancelDeleteFolder" class="btn btn-secondary"
data-i18n-key="cancel">Cancel</button>
<button id="confirmDeleteFolder" class="btn btn-danger"
data-i18n-key="delete">Delete</button>
</div>
</div>
</div>
</div>
<div id="folderHelpTooltip" class="folder-help-tooltip"
style="display: none; position: absolute; top: 50px; right: 15px; background: #fff; border: 1px solid #ccc; padding: 10px; z-index: 1000; box-shadow: 2px 2px 6px rgba(0,0,0,0.2);">
<ul class="folder-help-list" style="margin: 0; padding-left: 20px;">
<li data-i18n-key="folder_help_item_1">Click on a folder in the tree to view its files.</li>
<li data-i18n-key="folder_help_item_2">Use [-] to collapse and [+] to expand folders.</li>
<li data-i18n-key="folder_help_item_3">Select a folder and click "Create Folder" to add a
subfolder.</li>
<li data-i18n-key="folder_help_item_4">To rename or delete a folder, select it and then click
the appropriate button.</li>
</ul>
</div>
</div>
</div>
</div>
</div> <!-- end uploadFolderRow -->
</div> <!-- end container -->
</div> <!-- end mainOperations -->
<!-- File List Section -->
<div id="fileListContainer" style="display: none;">
<h2 id="fileListTitle" data-i18n-key="file_list_title">Files in (Root)</h2>
<div id="fileListActions" class="file-list-actions">
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;"
data-i18n-key="delete_files">Delete Files</button>
<div id="deleteFilesModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="delete_selected_files_title">Delete Selected Files</h4>
<p id="deleteFilesMessage" data-i18n-key="delete_files_message">Are you sure you want to delete the
selected files?</p>
<div class="modal-footer">
<button id="cancelDeleteFiles" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmDeleteFiles" class="btn btn-danger" data-i18n-key="delete">Delete</button>
</div>
</div>
</div>
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled
data-i18n-key="copy_files">Copy Files</button>
<div id="copyFilesModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="copy_files_title">Copy Selected Files</h4>
<p id="copyFilesMessage" data-i18n-key="copy_files_message">Select a target folder for copying the
selected files:</p>
<select id="copyTargetFolder" class="form-control modal-input"></select>
<div class="modal-footer">
<button id="cancelCopyFiles" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmCopyFiles" class="btn btn-primary" data-i18n-key="copy">Copy</button>
</div>
</div>
</div>
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled
data-i18n-key="move_files">Move Files</button>
<div id="moveFilesModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="move_files_title">Move Selected Files</h4>
<p id="moveFilesMessage" data-i18n-key="move_files_message">Select a target folder for moving the
selected files:</p>
<select id="moveTargetFolder" class="form-control modal-input"></select>
<div class="modal-footer">
<button id="cancelMoveFiles" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmMoveFiles" class="btn btn-primary" data-i18n-key="move">Move</button>
</div>
</div>
</div>
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled
data-i18n-key="download_zip">Download ZIP</button>
<button id="extractZipBtn" class="btn btn-sm btn-info" data-i18n-title="extract_zip"
data-i18n-key="extract_zip_button">Extract Zip</button>
<div id="downloadZipModal" class="modal" style="display:none;">
<div class="modal-content">
<h4 data-i18n-key="download_zip_title">Download Selected Files as Zip</h4>
<p data-i18n-key="download_zip_prompt">Enter a name for the zip file:</p>
<input type="text" id="zipFileNameInput" class="form-control" data-i18n-placeholder="zip_placeholder"
placeholder="files.zip" />
<div class="modal-footer" style="margin-top:15px; text-align:right;">
<button id="cancelDownloadZip" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmDownloadZip" class="btn btn-primary" data-i18n-key="download">Download</button>
</div>
</div>
</div>
</div>
<div id="fileList"></div>
</div>
</div> <!-- end container-fluid -->
</div> <!-- end mainColumn -->
</div> <!-- end main-wrapper -->
<!-- Download Progress Modal -->
<div id="downloadProgressModal" class="modal" style="display: none;">
<div class="modal-content" style="text-align: center; padding: 20px;">
<h4 id="downloadProgressTitle" data-i18n-key="preparing_download">
Preparing your download...
</h4>
<!-- spinner -->
<span class="material-icons download-spinner">autorenew</span>
<!-- these were missing -->
<progress id="downloadProgressBar" value="0" max="100" style="width:100%; height:1.5em; display:none;"></progress>
<p>
<span id="downloadProgressPercent" style="display:none;">0%</span>
</p>
</div>
</div>
<!-- Single File Download Modal -->
<div id="downloadFileModal" class="modal" style="display: none;">
<div class="modal-content" style="text-align: center; padding: 20px;">
<h4 data-i18n-key="download_file">Download File</h4>
<p data-i18n-key="confirm_or_change_filename">Confirm or change the download file name:</p>
<input type="text" id="downloadFileNameInput" class="form-control" data-i18n-placeholder="filename"
placeholder="Filename" />
<div style="margin-top: 15px; text-align: right;">
<button id="cancelDownloadFile" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmSingleDownloadButton" class="btn btn-primary" data-i18n-key="download">Download</button>
</div>
</div>
</div>
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
<div id="changePasswordModal" class="modal" style="display:none;">
<div class="modal-content" style="max-width:400px; margin:auto;">
<span id="closeChangePasswordModal"
class="editor-close-btn">&times;</span>
<h3 data-i18n-key="change_password_title">Change Password</h3>
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="newPassword" class="form-control" data-i18n-placeholder="new_password"
placeholder="New Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="confirmPassword" class="form-control" data-i18n-placeholder="confirm_new_password"
placeholder="Confirm New Password" style="width:100%; margin: 5px 0;" />
<button id="saveNewPasswordBtn" class="btn btn-primary" data-i18n-key="save" style="width:100%;">Save</button>
</div>
</div>
<div id="addUserModal" class="modal" style="display:none;">
<div class="modal-content">
<h3 data-i18n-key="create_new_user_title">Create New User</h3>
<!-- 1) Add a form around these fields -->
<form id="addUserForm">
<label for="newUsername" data-i18n-key="username">Username:</label>
<input type="text" id="newUsername" class="form-control" required />
<label for="addUserPassword" data-i18n-key="password">Password:</label>
<input type="password" id="addUserPassword" class="form-control" required />
<div id="adminCheckboxContainer">
<input type="checkbox" id="isAdmin" />
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
</div>
<div class="button-container">
<!-- Cancel stays type="button" -->
<button type="button" id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">
Cancel
</button>
<!-- Save becomes type="submit" -->
<button type="submit" id="saveUserBtn" class="btn btn-primary" data-i18n-key="save_user">
Save User
</button>
</div>
</form>
</div>
</div>
<div id="removeUserModal" class="modal" style="display:none;">
<div class="modal-content">
<h3 data-i18n-key="remove_user_title">Remove User</h3>
<label for="removeUsernameSelect" data-i18n-key="select_user_remove">Select a user to remove:</label>
<select id="removeUsernameSelect" class="form-control"></select>
<div class="button-container">
<button id="cancelRemoveUserBtn" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="deleteUserBtn" class="btn btn-danger" data-i18n-key="delete_user">Delete User</button>
</div>
</div>
</div>
<div id="renameFileModal" class="modal" style="display:none;">
<div class="modal-content">
<h4 data-i18n-key="rename_file_title">Rename File</h4>
<input type="text" id="newFileName" class="form-control" data-i18n-placeholder="rename_file_placeholder"
placeholder="Enter new file name" style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFile" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="submitRenameFile" class="btn btn-primary" data-i18n-key="rename">Rename</button>
</div>
</div>
</div>
<div id="customConfirmModal" class="modal" style="display:none;">
<div class="modal-content">
<p id="confirmMessage"></p>
<div class="modal-actions">
<button id="confirmYesBtn" class="btn btn-primary" data-i18n-key="yes">Yes</button>
<button id="confirmNoBtn" class="btn btn-secondary" data-i18n-key="no">No</button>
</div>
</div>
</div>
<script type="module" src="js/main.js"></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More