Compare commits
179 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1726f0160 | ||
|
|
bd1841b788 | ||
|
|
bde35d1d31 | ||
|
|
8d6a1be777 | ||
|
|
56f34ba362 | ||
|
|
4d329e046f | ||
|
|
f3977153fb | ||
|
|
274bedd186 | ||
|
|
2e4dbe7f7f | ||
|
|
0334e443eb | ||
|
|
76f5ed5c96 | ||
|
|
18f588dc24 | ||
|
|
491c686762 | ||
|
|
25303df677 | ||
|
|
ae0d63b86f | ||
|
|
41ade2e205 | ||
|
|
0a9d332d60 | ||
|
|
1983f7705f | ||
|
|
6b2bf0ba70 | ||
|
|
6d9715169c | ||
|
|
0645a3712a | ||
|
|
ebc32ea965 | ||
|
|
078db33458 | ||
|
|
04f5cbe31f | ||
|
|
b5a7d8d559 | ||
|
|
58f8485b02 | ||
|
|
3e1da9c335 | ||
|
|
6bf6206e1c | ||
|
|
f9c60951c9 | ||
|
|
06b3f28df0 | ||
|
|
89f124250c | ||
|
|
66f13fd6a7 | ||
|
|
a81d9cb940 | ||
|
|
13b8871200 | ||
|
|
2792c05c1c | ||
|
|
6ccfc88acb | ||
|
|
7f1d59b33a | ||
|
|
e4e8b108d2 | ||
|
|
242661a9c9 | ||
|
|
ca3e2f316c | ||
|
|
6ff4aa5f34 | ||
|
|
1eb54b8e6e | ||
|
|
4a6c424540 | ||
|
|
d23d5b7f3f | ||
|
|
a48ba09f02 | ||
|
|
61357af203 | ||
|
|
e390a35e8a | ||
|
|
7e50ba1f70 | ||
|
|
cc41f8cc95 | ||
|
|
7c31b9689f | ||
|
|
461921b7bc | ||
|
|
3b58123584 | ||
|
|
cd9d7eb0ba | ||
|
|
c0c8d68dc4 | ||
|
|
2dfcb4062f | ||
|
|
d839b3ac1c | ||
|
|
766458f707 | ||
|
|
22cce5a898 | ||
|
|
75d3bf5a9b | ||
|
|
4ec4ba832f | ||
|
|
97b67593bc | ||
|
|
ec5c3fc452 | ||
|
|
853d8835d9 | ||
|
|
1d36d002c6 | ||
|
|
844976ef89 | ||
|
|
66e0d7ecbe | ||
|
|
a5fbcdef88 | ||
|
|
a897d1734f | ||
|
|
a9c4200827 | ||
|
|
97559873dc | ||
|
|
0683b27534 | ||
|
|
49c42e8096 | ||
|
|
ed39e112a9 | ||
|
|
25edab923a | ||
|
|
b8ae3c4402 | ||
|
|
fb537b1d61 | ||
|
|
90439022e3 | ||
|
|
b4c8738b8a | ||
|
|
e193bf9b13 | ||
|
|
a70d8fc2c7 | ||
|
|
d9f69d7917 | ||
|
|
28ac23c2f6 | ||
|
|
b06c49f213 | ||
|
|
8553efabc1 | ||
|
|
81a08ffd5b | ||
|
|
296dae96a5 | ||
|
|
337f529afd | ||
|
|
4360f2830a | ||
|
|
894cc938a5 | ||
|
|
01801ba950 | ||
|
|
5b592575a4 | ||
|
|
7cce03d092 | ||
|
|
ff92a6d26c | ||
|
|
4fa5faa2bf | ||
|
|
98850a7c65 | ||
|
|
15bac15c33 | ||
|
|
b2ff3efb3b | ||
|
|
b9ce3f92a4 | ||
|
|
f65b151bc3 | ||
|
|
703c93db25 | ||
|
|
d0353b137b | ||
|
|
a6c4c1d39c | ||
|
|
7aa4fe142a | ||
|
|
9f8337574a | ||
|
|
82eadebe3b | ||
|
|
9701747214 | ||
|
|
6ff25ed426 | ||
|
|
ecc41bfe31 | ||
|
|
94055d2c92 | ||
|
|
5b50400f28 | ||
|
|
688a4bcf52 | ||
|
|
4bcbb08650 | ||
|
|
1a044145ab | ||
|
|
59299cdbed | ||
|
|
4f74090818 | ||
|
|
70163d22f0 | ||
|
|
b4445fc4d8 | ||
|
|
4022ccde84 | ||
|
|
8d370fd1bb | ||
|
|
5100e8bf3b | ||
|
|
899b04e49a | ||
|
|
07053a6b9a | ||
|
|
58db1d49ac | ||
|
|
a2d678ee19 | ||
|
|
da62e70c02 | ||
|
|
f19d30f58a | ||
|
|
a8202adbec | ||
|
|
5dc58ffa42 | ||
|
|
f4f700ecda | ||
|
|
94178775d5 | ||
|
|
1d3f731483 | ||
|
|
6926d5b065 | ||
|
|
46e9761cae | ||
|
|
fa828f5dea | ||
|
|
3a86903827 | ||
|
|
4feef5700d | ||
|
|
41e2b5af90 | ||
|
|
27f071ba6e | ||
|
|
9020251ed5 | ||
|
|
84822e699e | ||
|
|
3d57efba6c | ||
|
|
7c3ce0803a | ||
|
|
119aefc209 | ||
|
|
52ddf8268f | ||
|
|
8d7187d538 | ||
|
|
394e7ef041 | ||
|
|
9c71c46c4e | ||
|
|
d228dc10b0 | ||
|
|
3f1007b1b3 | ||
|
|
27de0a9a48 | ||
|
|
051544dc5a | ||
|
|
89777584cf | ||
|
|
ed47e3c3bc | ||
|
|
edd9094218 | ||
|
|
3b0083516b | ||
|
|
fee3b544dd | ||
|
|
99ed05d3de | ||
|
|
32469778dc | ||
|
|
ecb4ac2c75 | ||
|
|
4ae509acd2 | ||
|
|
b1cd4b7bdc | ||
|
|
d57687adee | ||
|
|
64d41af21b | ||
|
|
a8f5a6d3bc | ||
|
|
062cfc0dd4 | ||
|
|
32d25b1b69 | ||
|
|
56626aaa40 | ||
|
|
0697fcb1df | ||
|
|
c08c903810 | ||
|
|
2c8374a66c | ||
|
|
49138835ce | ||
|
|
c0dc0ce391 | ||
|
|
6426f4b924 | ||
|
|
b72356b657 | ||
|
|
fc45767712 | ||
|
|
1d5c6a48b5 | ||
|
|
772326c8e0 | ||
|
|
5892236aa9 | ||
|
|
0215bd3d76 |
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
||||
# dockerignore
|
||||
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
.github/**
|
||||
Dockerfile*
|
||||
resources/
|
||||
node_modules/
|
||||
*.log
|
||||
tmp/
|
||||
.env
|
||||
.vscode/
|
||||
.DS_Store
|
||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
public/api.html linguist-documentation
|
||||
public/openapi.json linguist-documentation
|
||||
resources/ export-ignore
|
||||
.github/ export-ignore
|
||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
10
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
43
.github/workflows/sync-changelog.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Sync Changelog to Docker Repo
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'CHANGELOG.md'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout FileRise
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: file-rise
|
||||
|
||||
- name: Checkout filerise-docker
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: error311/filerise-docker
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
path: docker-repo
|
||||
|
||||
- name: Copy CHANGELOG.md
|
||||
run: |
|
||||
cp file-rise/CHANGELOG.md docker-repo/CHANGELOG.md
|
||||
|
||||
- name: Commit & push
|
||||
working-directory: docker-repo
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add CHANGELOG.md
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "chore: sync CHANGELOG.md from FileRise"
|
||||
git push origin main
|
||||
fi
|
||||
1347
CHANGELOG.md
Normal file
1347
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
243
CONTRIBUTING.md
Normal file
243
CONTRIBUTING.md
Normal 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 project’s 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 they’re 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). We’re 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
140
Dockerfile
Normal 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"]
|
||||
1
LICENSE
1
LICENSE
@@ -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
|
||||
|
||||
408
README.md
408
README.md
@@ -1,264 +1,256 @@
|
||||
# MFE - Lightweight Multi File Upload Editor
|
||||
# FileRise
|
||||
|
||||
**Video demo:**
|
||||
**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.
|
||||
|
||||
https://github.com/user-attachments/assets/179e6940-5798-4482-9a69-696f806c37de
|
||||
**4/3/2025 Video demo:**
|
||||
|
||||
<https://github.com/user-attachments/assets/221f6a53-85f5-48d4-9abe-89445e0af90e>
|
||||
|
||||
**Dark mode:**
|
||||

|
||||
|
||||
changelogs available here: <https://github.com/error311/multi-file-upload-editor-docker/>
|
||||
|
||||
MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web application for uploading, syntax highlight editing, drag & drop 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 TinyFileManager or ProjectSend, providing an easy-to-setup experience ideal for document management, image galleries, firmware file hosting, and more.
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
## Features at a Glance or [Full Features Wiki](https://github.com/error311/FileRise/wiki/Features)
|
||||
|
||||
- **Multiple File/Folder Uploads with Progress (Resumable.js Integration):**
|
||||
- Users can effortlessly upload multiple files and folders simultaneously by either selecting them through the file picker or dragging and dropping them directly into the interface.
|
||||
- **Chunked Uploads:** Files are uploaded in configurable chunks (default set as 3 MB) to efficiently handle large files.
|
||||
- **Pause, Resume, and Retry:** Uploads can be paused and resumed at any time, with support for retrying failed chunks.
|
||||
- **Real-Time Progress:** Each file shows an individual progress bar that displays percentage complete and upload speed.
|
||||
- **File & Folder Grouping:** When many files are dropped, files are automatically grouped into a scrollable wrapper, ensuring the interface remains clean.
|
||||
- **Secure Uploads:** All uploads integrate CSRF token validation and other security checks.
|
||||
- **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 or breadcrumb. 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.
|
||||
- **Intuitive Breadcrumb Navigation:** Clickable breadcrumbs enable users to quickly jump to any parent folder, streamlining navigation across subfolders. Supports drag & drop to move files.
|
||||
- **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 PHP’s `password_hash()` for security.
|
||||
- All state-changing endpoints include CSRF token validation.
|
||||
- Change password supported for all users.
|
||||
- **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 system’s 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 head‑less from the CLI**. Standard WebDAV operations (upload / download / rename / delete) work in Cyberduck, WinSCP, GNOME Files, Finder, etc., and you can also script against it with `curl` – see the [WebDAV](https://github.com/error311/FileRise/wiki/WebDAV) + [curl](https://github.com/error311/FileRise/wiki/Accessing-FileRise-via-curl%C2%A0(WebDAV)) quick‑start for examples. Folder‑Only users are restricted to their personal directory, while admins and unrestricted users have full access.
|
||||
|
||||
- 📚 **API Documentation:** Fully auto‑generated OpenAPI spec (`openapi.json`) and interactive HTML docs (`api.html`) powered by Redoc.
|
||||
|
||||
- 📝 **Built-in Editor & Preview:** View images, videos, audio, and PDFs inline with a preview modal – no need to download just to see them. Edit text/code files right in your browser with a CodeMirror-based editor featuring syntax highlighting and line numbers. Great for config files or notes – tweak and save changes without leaving FileRise.
|
||||
|
||||
- 🏷️ **Tags & Search:** Categorize your files with color-coded tags 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 & Self‑Contained:** FileRise runs on PHP 8.1+ with no external database required – data is stored in files (users, metadata) for simplicity. It’s a single‑folder web app you can drop into any Apache/PHP server or run as a container. Docker & Unraid ready: use our pre‑built image for a hassle‑free setup. Memory and CPU footprint is minimal, yet the app scales to thousands of files with pagination and sorting features.
|
||||
|
||||
(For a full list of features and detailed changelogs, see the [Wiki](https://github.com/error311/FileRise/wiki), [changelog](https://github.com/error311/FileRise/blob/master/CHANGELOG.md) or the [releases](https://github.com/error311/FileRise/releases) pages.)
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
## Live Demo
|
||||
|
||||
**Light mode:**
|
||||

|
||||
|
||||
**Dark editor:**
|
||||

|
||||
|
||||
**Dark preview**
|
||||

|
||||
|
||||
**Restore or Delete Trash:**
|
||||

|
||||
|
||||
**Login page:**
|
||||

|
||||
|
||||
**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 app’s base URL.
|
||||
- `UPLOAD_DIR`: Adjust the directory path for uploads.
|
||||
- `TIMEZONE`: Set to your preferred timezone.
|
||||
- `TOTAL_UPLOAD_SIZE`: Ensure it matches PHP’s `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 server’s 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 you’re 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 server’s 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 don’t 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 PHP’s `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 don’t 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, you’ll 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, you’ll be prompted to create the Admin user (same as Docker setup). After that, the application is ready to use!
|
||||
|
||||
---
|
||||
|
||||
## Docker Usage
|
||||
## Quick‑start: 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 PHP’s `post_max_size` / `upload_max_filesize` are all set high enough. For extremely large files, you might also need to increase max_execution_time in PHP or rely on the resumable upload feature in smaller chunks.
|
||||
|
||||
- **BASE_URL:**
|
||||
Set to the URL where your application is hosted (e.g., `http://yourdomain.com/uploads/`).
|
||||
- **How to enable HTTPS?** FileRise itself doesn’t 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 user’s 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 PHP’s `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 container’s 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 you’re free to use, modify, and distribute **FileRise**, with attribution. We hope you find it useful and contribute back!
|
||||
|
||||
33
SECURITY.md
Normal file
33
SECURITY.md
Normal 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).
|
||||
86
addUser.php
86
addUser.php
@@ -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 (zero bytes or trimmed content is empty)
|
||||
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
|
||||
if ($isSetup && (!file_exists($usersFile) || filesize($usersFile) == 0 || trim(file_get_contents($usersFile)) === '')) {
|
||||
// Allow initial admin creation without session checks.
|
||||
$setupMode = true;
|
||||
} else {
|
||||
$setupMode = false;
|
||||
// In non-setup mode, check CSRF token and require admin privileges.
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
if (
|
||||
!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
||||
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true
|
||||
) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Get input data from JSON.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
$newUsername = trim($data["username"] ?? "");
|
||||
$newPassword = trim($data["password"] ?? "");
|
||||
|
||||
// In setup mode, force the new user to be admin.
|
||||
if ($setupMode) {
|
||||
$isAdmin = "1";
|
||||
} else {
|
||||
$isAdmin = !empty($data["isAdmin"]) ? "1" : "0"; // "1" for admin, "0" for regular user.
|
||||
}
|
||||
|
||||
// Validate input.
|
||||
if (!$newUsername || !$newPassword) {
|
||||
echo json_encode(["error" => "Username and password required"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate username using preg_match (allow letters, numbers, underscores, dashes, and spaces).
|
||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $newUsername)) {
|
||||
echo json_encode(["error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure users.txt exists.
|
||||
if (!file_exists($usersFile)) {
|
||||
file_put_contents($usersFile, '');
|
||||
}
|
||||
|
||||
// Check if username already exists.
|
||||
$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($existingUsers as $line) {
|
||||
list($storedUser, $storedHash, $storedRole) = explode(':', trim($line));
|
||||
if ($newUsername === $storedUser) {
|
||||
echo json_encode(["error" => "User already exists"]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the password.
|
||||
$hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
|
||||
|
||||
// Prepare new user line.
|
||||
$newUserLine = $newUsername . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL;
|
||||
|
||||
// In setup mode, overwrite users.txt; otherwise, append to it.
|
||||
if ($setupMode) {
|
||||
file_put_contents($usersFile, $newUserLine);
|
||||
} else {
|
||||
file_put_contents($usersFile, $newUserLine, FILE_APPEND);
|
||||
}
|
||||
|
||||
echo json_encode(["success" => "User added successfully"]);
|
||||
?>
|
||||
353
auth.js
353
auth.js
@@ -1,353 +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) {
|
||||
if (headerButtons.children.length >= 5) {
|
||||
headerButtons.insertBefore(restoreBtn, headerButtons.children[5]);
|
||||
} 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";
|
||||
const restoreBtn = document.getElementById("restoreFilesBtn");
|
||||
if (restoreBtn) {
|
||||
restoreBtn.style.display = "none";
|
||||
}
|
||||
}
|
||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||
if (selectElem) {
|
||||
const stored = localStorage.getItem("itemsPerPage") || "10";
|
||||
selectElem.value = stored;
|
||||
}
|
||||
} else {
|
||||
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.
|
||||
const authForm = document.getElementById("authForm");
|
||||
if (authForm) {
|
||||
authForm.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
// Get the "Remember me" checkbox value.
|
||||
const rememberMe = document.getElementById("rememberMeCheckbox")
|
||||
? document.getElementById("rememberMeCheckbox").checked
|
||||
: false;
|
||||
const formData = {
|
||||
username: document.getElementById("loginUsername").value.trim(),
|
||||
password: document.getElementById("loginPassword").value.trim(),
|
||||
remember_me: rememberMe
|
||||
};
|
||||
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 {
|
||||
if (data.error && data.error.includes("Too many failed login attempts")) {
|
||||
showToast(data.error);
|
||||
const loginButton = authForm.querySelector("button[type='submit']");
|
||||
if (loginButton) {
|
||||
loginButton.disabled = true;
|
||||
setTimeout(() => {
|
||||
loginButton.disabled = false;
|
||||
showToast("You can now try logging in again.");
|
||||
}, 30 * 60 * 1000);
|
||||
}
|
||||
} else {
|
||||
showToast("Login failed: " + (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();
|
||||
// Use the new ID for the add user modal's password field.
|
||||
const newPassword = document.getElementById("addUserPassword").value.trim();
|
||||
const isAdmin = document.getElementById("isAdmin").checked;
|
||||
console.log("newUsername:", newUsername, "newPassword:", newPassword);
|
||||
if (!newUsername || !newPassword) {
|
||||
showToast("Username and password are required!");
|
||||
return;
|
||||
}
|
||||
let url = "addUser.php";
|
||||
if (window.setupMode) {
|
||||
url += "?setup=1";
|
||||
}
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("User added successfully!");
|
||||
closeAddUserModal();
|
||||
checkAuthentication(false);
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not add user"));
|
||||
}
|
||||
})
|
||||
.catch(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();
|
||||
});
|
||||
|
||||
document.getElementById("changePasswordBtn").addEventListener("click", function() {
|
||||
// Show the Change Password modal.
|
||||
document.getElementById("changePasswordModal").style.display = "block";
|
||||
});
|
||||
|
||||
document.getElementById("closeChangePasswordModal").addEventListener("click", function() {
|
||||
// Hide the Change Password modal.
|
||||
document.getElementById("changePasswordModal").style.display = "none";
|
||||
});
|
||||
|
||||
document.getElementById("saveNewPasswordBtn").addEventListener("click", function() {
|
||||
const oldPassword = document.getElementById("oldPassword").value.trim();
|
||||
const newPassword = document.getElementById("newPassword").value.trim(); // Change Password modal field
|
||||
const confirmPassword = document.getElementById("confirmPassword").value.trim();
|
||||
|
||||
if (!oldPassword || !newPassword || !confirmPassword) {
|
||||
showToast("Please fill in all fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showToast("New passwords do not match.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the data to send.
|
||||
const data = { oldPassword, newPassword, confirmPassword };
|
||||
|
||||
// Send request to changePassword.php.
|
||||
fetch("changePassword.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
showToast(result.success);
|
||||
// Clear form fields and close modal.
|
||||
document.getElementById("oldPassword").value = "";
|
||||
document.getElementById("newPassword").value = "";
|
||||
document.getElementById("confirmPassword").value = "";
|
||||
document.getElementById("changePasswordModal").style.display = "none";
|
||||
} else {
|
||||
showToast("Error: " + (result.error || "Could not change password."));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error changing password:", error);
|
||||
showToast("Error changing password.");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkAuthentication(showLoginToast = true) {
|
||||
return sendRequest("checkAuth.php")
|
||||
.then(data => {
|
||||
if (data.setup) {
|
||||
window.setupMode = true;
|
||||
if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user.");
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
toggleVisibility("addUserModal", true);
|
||||
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("addUserPassword").value = ""; // Updated for add user modal
|
||||
}
|
||||
|
||||
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 };
|
||||
137
auth.php
137
auth.php
@@ -1,137 +0,0 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
|
||||
// --- Brute Force Protection Settings ---
|
||||
$maxAttempts = 5;
|
||||
$lockoutTime = 30 * 60; // 30 minutes in seconds
|
||||
$attemptsFile = USERS_DIR . 'failed_logins.json'; // JSON file for tracking failed login attempts
|
||||
$failedLogFile = USERS_DIR . 'failed_login.log'; // Plain text log for fail2ban
|
||||
|
||||
// Persistent tokens file for "Remember me"
|
||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
||||
|
||||
// Load failed attempts data from file.
|
||||
function loadFailedAttempts($file) {
|
||||
if (file_exists($file)) {
|
||||
$data = json_decode(file_get_contents($file), true);
|
||||
if (is_array($data)) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Save failed attempts data to file.
|
||||
function saveFailedAttempts($file, $data) {
|
||||
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
// Get current IP address.
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
$currentTime = time();
|
||||
|
||||
// Load failed attempts.
|
||||
$failedAttempts = loadFailedAttempts($attemptsFile);
|
||||
|
||||
// Check if this IP is currently locked out.
|
||||
if (isset($failedAttempts[$ip])) {
|
||||
$attemptData = $failedAttempts[$ip];
|
||||
if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) {
|
||||
echo json_encode(["error" => "Too many failed login attempts. Please try again later."]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Authentication Function ---
|
||||
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"] ?? "");
|
||||
$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true;
|
||||
|
||||
// Validate input: ensure both fields are provided.
|
||||
if (!$username || !$password) {
|
||||
echo json_encode(["error" => "Username and password are required"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate username format.
|
||||
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;
|
||||
}
|
||||
|
||||
// Attempt to authenticate the user.
|
||||
$userRole = authenticate($username, $password);
|
||||
if ($userRole !== false) {
|
||||
// On successful login, reset failed attempts for this IP.
|
||||
if (isset($failedAttempts[$ip])) {
|
||||
unset($failedAttempts[$ip]);
|
||||
saveFailedAttempts($attemptsFile, $failedAttempts);
|
||||
}
|
||||
// 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
|
||||
|
||||
// If "Remember me" is checked, generate a persistent login token.
|
||||
if ($rememberMe) {
|
||||
// Generate a secure random token.
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiry = time() + (30 * 24 * 60 * 60); // 30 days
|
||||
// Load existing persistent tokens.
|
||||
$persistentTokens = [];
|
||||
if (file_exists($persistentTokensFile)) {
|
||||
$persistentTokens = json_decode(file_get_contents($persistentTokensFile), true);
|
||||
if (!is_array($persistentTokens)) {
|
||||
$persistentTokens = [];
|
||||
}
|
||||
}
|
||||
// Save token along with username and expiry.
|
||||
$persistentTokens[$token] = [
|
||||
"username" => $username,
|
||||
"expiry" => $expiry
|
||||
];
|
||||
file_put_contents($persistentTokensFile, json_encode($persistentTokens, JSON_PRETTY_PRINT));
|
||||
// Set the cookie. (Assuming $secure is defined in config.php.)
|
||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||
}
|
||||
|
||||
echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]);
|
||||
} else {
|
||||
// On failed login, update failed attempts.
|
||||
if (isset($failedAttempts[$ip])) {
|
||||
$failedAttempts[$ip]['count']++;
|
||||
$failedAttempts[$ip]['last_attempt'] = $currentTime;
|
||||
} else {
|
||||
$failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime];
|
||||
}
|
||||
saveFailedAttempts($attemptsFile, $failedAttempts);
|
||||
|
||||
// Log the failed attempt to the plain text log for fail2ban.
|
||||
$logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL;
|
||||
file_put_contents($failedLogFile, $logLine, FILE_APPEND);
|
||||
|
||||
echo json_encode(["error" => "Invalid credentials"]);
|
||||
}
|
||||
?>
|
||||
@@ -1,85 +0,0 @@
|
||||
<?php
|
||||
// changePassword.php
|
||||
require 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Make sure the user is logged in.
|
||||
session_start();
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if (!$username) {
|
||||
echo json_encode(["error" => "No username in session"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// CSRF token check.
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get POST data.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
$oldPassword = trim($data["oldPassword"] ?? "");
|
||||
$newPassword = trim($data["newPassword"] ?? "");
|
||||
$confirmPassword = trim($data["confirmPassword"] ?? "");
|
||||
|
||||
// Validate input.
|
||||
if (!$oldPassword || !$newPassword || !$confirmPassword) {
|
||||
echo json_encode(["error" => "All fields are required."]);
|
||||
exit;
|
||||
}
|
||||
if ($newPassword !== $confirmPassword) {
|
||||
echo json_encode(["error" => "New passwords do not match."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Path to users file.
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
if (!file_exists($usersFile)) {
|
||||
echo json_encode(["error" => "Users file not found"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Read current users.
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$userFound = false;
|
||||
$newLines = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
list($storedUser, $storedHash, $storedRole) = explode(':', trim($line));
|
||||
if ($storedUser === $username) {
|
||||
$userFound = true;
|
||||
// Verify the old password.
|
||||
if (!password_verify($oldPassword, $storedHash)) {
|
||||
echo json_encode(["error" => "Old password is incorrect."]);
|
||||
exit;
|
||||
}
|
||||
// Hash the new password.
|
||||
$newHashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
|
||||
// Rebuild the line with the new hash.
|
||||
$newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole;
|
||||
} else {
|
||||
$newLines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$userFound) {
|
||||
echo json_encode(["error" => "User not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Save updated users file.
|
||||
if (file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL)) {
|
||||
echo json_encode(["success" => "Password updated successfully."]);
|
||||
} else {
|
||||
echo json_encode(["error" => "Could not update password."]);
|
||||
}
|
||||
?>
|
||||
@@ -1,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
12
composer.json
Normal 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
1040
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
78
config.php
78
config.php
@@ -1,78 +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));
|
||||
}
|
||||
|
||||
// Auto-login via persistent token if session is not active.
|
||||
if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token'])) {
|
||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
||||
if (file_exists($persistentTokensFile)) {
|
||||
$persistentTokens = json_decode(file_get_contents($persistentTokensFile), true);
|
||||
if (is_array($persistentTokens) && isset($persistentTokens[$_COOKIE['remember_me_token']])) {
|
||||
$tokenData = $persistentTokens[$_COOKIE['remember_me_token']];
|
||||
if ($tokenData['expiry'] >= time()) {
|
||||
// Token is valid; auto-authenticate the user.
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $tokenData["username"];
|
||||
// Optionally, set admin status if stored in token data:
|
||||
// $_SESSION["isAdmin"] = $tokenData["isAdmin"];
|
||||
} else {
|
||||
// Token expired; remove it and clear the cookie.
|
||||
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
||||
file_put_contents($persistentTokensFile, json_encode($persistentTokens, JSON_PRETTY_PRINT));
|
||||
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
?>
|
||||
152
config/config.php
Normal file
152
config/config.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?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', '/^[\p{L}\p{N}\p{M}%\-\.\(\) _]+$/u');
|
||||
define('REGEX_USER', '/^[\p{L}\p{N}_\- ]+$/u');
|
||||
|
||||
date_default_timezone_set(TIMEZONE);
|
||||
|
||||
// Encryption helpers
|
||||
function encryptData($data, $encryptionKey)
|
||||
{
|
||||
$cipher = 'AES-256-CBC';
|
||||
$ivlen = openssl_cipher_iv_length($cipher);
|
||||
$iv = openssl_random_pseudo_bytes($ivlen);
|
||||
$ct = openssl_encrypt($data, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||
return base64_encode($iv . $ct);
|
||||
}
|
||||
|
||||
function decryptData($encryptedData, $encryptionKey)
|
||||
{
|
||||
$cipher = 'AES-256-CBC';
|
||||
$data = base64_decode($encryptedData);
|
||||
$ivlen = openssl_cipher_iv_length($cipher);
|
||||
$iv = substr($data, 0, $ivlen);
|
||||
$ct = substr($data, $ivlen);
|
||||
return openssl_decrypt($ct, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||
}
|
||||
|
||||
// Load encryption key
|
||||
$envKey = getenv('PERSISTENT_TOKENS_KEY');
|
||||
if ($envKey === false || $envKey === '') {
|
||||
$encryptionKey = 'default_please_change_this_key';
|
||||
error_log('WARNING: Using default encryption key. Please set PERSISTENT_TOKENS_KEY in your environment.');
|
||||
} else {
|
||||
$encryptionKey = $envKey;
|
||||
}
|
||||
|
||||
// Helper to load JSON permissions (with optional decryption)
|
||||
function loadUserPermissions($username)
|
||||
{
|
||||
global $encryptionKey;
|
||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||
if (file_exists($permissionsFile)) {
|
||||
$content = file_get_contents($permissionsFile);
|
||||
$decrypted = decryptData($content, $encryptionKey);
|
||||
$json = ($decrypted !== false) ? $decrypted : $content;
|
||||
$perms = json_decode($json, true);
|
||||
if (is_array($perms) && isset($perms[$username])) {
|
||||
return !empty($perms[$username]) ? $perms[$username] : false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine HTTPS usage
|
||||
$envSecure = getenv('SECURE');
|
||||
$secure = ($envSecure !== false)
|
||||
? filter_var($envSecure, FILTER_VALIDATE_BOOLEAN)
|
||||
: (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
|
||||
// Choose session lifetime based on "remember me" cookie
|
||||
$defaultSession = 7200; // 2 hours
|
||||
$persistentDays = 30 * 24 * 60 * 60; // 30 days
|
||||
$sessionLifetime = isset($_COOKIE['remember_me_token'])
|
||||
? $persistentDays
|
||||
: $defaultSession;
|
||||
|
||||
// Configure PHP session cookie and GC
|
||||
session_set_cookie_params([
|
||||
'lifetime' => $sessionLifetime,
|
||||
'path' => '/',
|
||||
'domain' => '', // adjust if you need a specific domain
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
]);
|
||||
ini_set('session.gc_maxlifetime', (string)$sessionLifetime);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// CSRF token
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
// Auto‑login via persistent token
|
||||
if (empty($_SESSION["authenticated"]) && !empty($_COOKIE['remember_me_token'])) {
|
||||
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||
$tokens = [];
|
||||
if (file_exists($tokFile)) {
|
||||
$enc = file_get_contents($tokFile);
|
||||
$dec = decryptData($enc, $encryptionKey);
|
||||
$tokens = json_decode($dec, true) ?: [];
|
||||
}
|
||||
$token = $_COOKIE['remember_me_token'];
|
||||
if (!empty($tokens[$token])) {
|
||||
$data = $tokens[$token];
|
||||
if ($data['expiry'] >= time()) {
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $data["username"];
|
||||
$_SESSION["folderOnly"] = loadUserPermissions($data["username"]);
|
||||
$_SESSION["isAdmin"] = !empty($data["isAdmin"]);
|
||||
} else {
|
||||
// expired — clean up
|
||||
unset($tokens[$token]);
|
||||
file_put_contents($tokFile, encryptData(json_encode($tokens, JSON_PRETTY_PRINT), $encryptionKey), LOCK_EX);
|
||||
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
143
copyFiles.php
143
copyFiles.php
@@ -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)]);
|
||||
}
|
||||
?>
|
||||
@@ -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.']);
|
||||
}
|
||||
?>
|
||||
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
// createShareLink.php
|
||||
require_once 'config.php';
|
||||
|
||||
// Get POST input.
|
||||
$input = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$input) {
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = isset($input['folder']) ? trim($input['folder']) : "";
|
||||
$file = isset($input['file']) ? basename($input['file']) : "";
|
||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
||||
$password = isset($input['password']) ? $input['password'] : "";
|
||||
|
||||
// Validate folder using regex.
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Generate a secure token.
|
||||
$token = bin2hex(random_bytes(16)); // 32 hex characters.
|
||||
|
||||
// Calculate expiration (Unix timestamp).
|
||||
$expires = time() + ($expirationMinutes * 60);
|
||||
|
||||
// Hash password if provided.
|
||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
|
||||
// File to store share links.
|
||||
$shareFile = META_DIR . "share_links.json";
|
||||
$shareLinks = [];
|
||||
if (file_exists($shareFile)) {
|
||||
$data = file_get_contents($shareFile);
|
||||
$shareLinks = json_decode($data, true);
|
||||
if (!is_array($shareLinks)) {
|
||||
$shareLinks = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up expired share links.
|
||||
$currentTime = time();
|
||||
foreach ($shareLinks as $key => $link) {
|
||||
if ($link["expires"] < $currentTime) {
|
||||
unset($shareLinks[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add record.
|
||||
$shareLinks[$token] = [
|
||||
"folder" => $folder,
|
||||
"file" => $file,
|
||||
"expires" => $expires,
|
||||
"password" => $hashedPassword
|
||||
];
|
||||
|
||||
// Save the share links.
|
||||
if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT))) {
|
||||
echo json_encode(["token" => $token, "expires" => $expires]);
|
||||
} else {
|
||||
echo json_encode(["error" => "Could not save share link."]);
|
||||
}
|
||||
?>
|
||||
53
custom-php.ini
Normal file
53
custom-php.ini
Normal file
@@ -0,0 +1,53 @@
|
||||
; custom-php.ini
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; OPcache Settings
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
opcache.enable=1
|
||||
opcache.enable_cli=0
|
||||
; Allocate 128MB of memory for opcode caching
|
||||
opcache.memory_consumption=128
|
||||
; Increase the maximum number of accelerated files (adjust if you have a large codebase)
|
||||
opcache.max_accelerated_files=4000
|
||||
; Refresh file timestamp every 60 seconds to avoid too many disk reads
|
||||
opcache.revalidate_freq=60
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Memory and Execution Time Limits
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Increase memory limit to 512M for large file processing or image processing operations
|
||||
memory_limit=512M
|
||||
; Set execution time limits to accommodate long-running uploads/processes
|
||||
max_execution_time=300
|
||||
max_input_time=300
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Realpath Cache Settings
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
realpath_cache_size=4096k
|
||||
realpath_cache_ttl=600
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; File Upload Settings
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Allow a maximum of 20 files per request
|
||||
max_file_uploads=20
|
||||
; Ensure the temporary directory is set (should exist and be writable)
|
||||
upload_tmp_dir=/tmp
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Session Configuration (if applicable)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
session.gc_maxlifetime=1440
|
||||
session.gc_probability=1
|
||||
session.gc_divisor=100
|
||||
session.save_path = "/var/www/sessions"
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Error Handling / Logging
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Do not display errors publicly in production
|
||||
display_errors=Off
|
||||
; Log errors to a dedicated file
|
||||
log_errors=On
|
||||
error_log=/var/log/php8.3-error.log
|
||||
147
deleteFiles.php
147
deleteFiles.php
@@ -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)]);
|
||||
}
|
||||
?>
|
||||
@@ -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.']);
|
||||
}
|
||||
?>
|
||||
@@ -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;
|
||||
?>
|
||||
308
domUtils.js
308
domUtils.js
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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">×</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";
|
||||
}
|
||||
59
download.php
59
download.php
@@ -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;
|
||||
?>
|
||||
133
downloadZip.php
133
downloadZip.php
@@ -1,133 +0,0 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if the user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Read and decode the JSON input.
|
||||
$rawData = file_get_contents("php://input");
|
||||
$data = json_decode($rawData, true);
|
||||
|
||||
if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = $data['folder'];
|
||||
$files = $data['files'];
|
||||
|
||||
// Validate folder name to allow subfolders.
|
||||
// "root" is allowed; otherwise, split by "/" and validate each segment.
|
||||
if ($folder !== "root") {
|
||||
$parts = explode('/', $folder);
|
||||
foreach ($parts as $part) {
|
||||
if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $part)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR;
|
||||
} else {
|
||||
$relativePath = "";
|
||||
}
|
||||
|
||||
// Use the absolute UPLOAD_DIR from config.php.
|
||||
$baseDir = realpath(UPLOAD_DIR);
|
||||
if ($baseDir === false) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Uploads directory not configured correctly."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath;
|
||||
$folderPathReal = realpath($folderPath);
|
||||
if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Folder not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($files)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "No files specified."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
foreach ($files as $fileName) {
|
||||
if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $fileName)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Invalid file name: " . $fileName]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Build an array of files to include in the ZIP.
|
||||
$filesToZip = [];
|
||||
foreach ($files as $fileName) {
|
||||
$filePath = $folderPathReal . DIRECTORY_SEPARATOR . $fileName;
|
||||
if (file_exists($filePath)) {
|
||||
$filesToZip[] = $filePath;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($filesToZip)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "No valid files found to zip."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Create a temporary file for the ZIP archive.
|
||||
$tempZip = tempnam(sys_get_temp_dir(), 'zip');
|
||||
unlink($tempZip); // Remove the temporary file so ZipArchive can create a new one.
|
||||
$tempZip .= '.zip';
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($tempZip, ZipArchive::CREATE) !== TRUE) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Could not create zip archive."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Add each file to the archive using its base name.
|
||||
foreach ($filesToZip as $filePath) {
|
||||
$zip->addFile($filePath, basename($filePath));
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
// Send headers to force download and disable caching.
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Disposition: attachment; filename="files.zip"');
|
||||
header('Content-Length: ' . filesize($tempZip));
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
// Output the file and delete it afterward.
|
||||
readfile($tempZip);
|
||||
unlink($tempZip);
|
||||
exit;
|
||||
?>
|
||||
1276
fileManager.js
1276
fileManager.js
File diff suppressed because it is too large
Load Diff
602
folderManager.js
602
folderManager.js
@@ -1,602 +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);
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Breadcrumb Functions
|
||||
// ----------------------
|
||||
// Render breadcrumb for a normalized folder path.
|
||||
// For example, if window.currentFolder is "Folder1/Folder1SubFolder2",
|
||||
// this will return: Root / Folder1 / Folder1SubFolder2.
|
||||
function renderBreadcrumb(normalizedFolder) {
|
||||
if (normalizedFolder === "root") {
|
||||
return `<span class="breadcrumb-link" data-folder="root">Root</span>`;
|
||||
}
|
||||
const parts = normalizedFolder.split("/");
|
||||
let breadcrumbItems = [];
|
||||
// Always start with "Root".
|
||||
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="root">Root</span>`);
|
||||
let cumulative = "";
|
||||
parts.forEach((part, index) => {
|
||||
cumulative = index === 0 ? part : cumulative + "/" + part;
|
||||
breadcrumbItems.push(`<span class="breadcrumb-separator"> / </span>`);
|
||||
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="${cumulative}">${escapeHTML(part)}</span>`);
|
||||
});
|
||||
return breadcrumbItems.join('');
|
||||
}
|
||||
|
||||
// Bind click and drag-and-drop events to breadcrumb links.
|
||||
function bindBreadcrumbEvents() {
|
||||
const breadcrumbLinks = document.querySelectorAll(".breadcrumb-link");
|
||||
breadcrumbLinks.forEach(link => {
|
||||
// Click event for navigation.
|
||||
link.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
let folder = this.getAttribute("data-folder");
|
||||
console.log("Breadcrumb clicked, folder:", folder);
|
||||
window.currentFolder = folder;
|
||||
localStorage.setItem("lastOpenedFolder", folder);
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
if (folder === "root") {
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
|
||||
} else {
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb(folder) + ")";
|
||||
}
|
||||
// Expand the folder tree to ensure the target is visible.
|
||||
expandTreePath(folder);
|
||||
// Update folder tree selection.
|
||||
document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||
const targetOption = document.querySelector(`.folder-option[data-folder="${folder}"]`);
|
||||
if (targetOption) {
|
||||
targetOption.classList.add("selected");
|
||||
}
|
||||
// Load the file list.
|
||||
loadFileList(folder);
|
||||
// Re-bind breadcrumb events to ensure all links remain active.
|
||||
bindBreadcrumbEvents();
|
||||
});
|
||||
|
||||
// Drag-and-drop events.
|
||||
link.addEventListener("dragover", function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.add("drop-hover");
|
||||
});
|
||||
link.addEventListener("dragleave", function (e) {
|
||||
this.classList.remove("drop-hover");
|
||||
});
|
||||
link.addEventListener("drop", function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.remove("drop-hover");
|
||||
const dropFolder = this.getAttribute("data-folder");
|
||||
let dragData;
|
||||
try {
|
||||
dragData = JSON.parse(e.dataTransfer.getData("application/json"));
|
||||
} catch (err) {
|
||||
console.error("Invalid drag data on breadcrumb:", err);
|
||||
return;
|
||||
}
|
||||
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 on breadcrumb:", error);
|
||||
showToast("Error moving files.");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// DOM Building Functions for Folder Tree
|
||||
// ----------------------
|
||||
|
||||
// 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') ? '[+]' : '[' + '<span class="custom-dash">-</span>' + ']';
|
||||
html += `<span class="folder-toggle" data-folder="${fullPath}">${toggleSymbol}</span>`;
|
||||
} else {
|
||||
html += `<span class="folder-indent-placeholder"></span>`;
|
||||
}
|
||||
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 normalized 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.innerHTML = "[" + '<span class="custom-dash">-</span>' + "]";
|
||||
let state = loadFolderTreeState();
|
||||
state[cumulative] = "block";
|
||||
saveFolderTreeState(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Drag & Drop Support for Folder Tree Nodes
|
||||
// ----------------------
|
||||
function folderDragOverHandler(event) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.classList.add("drop-hover");
|
||||
}
|
||||
|
||||
function folderDragLeaveHandler(event) {
|
||||
event.currentTarget.classList.remove("drop-hover");
|
||||
}
|
||||
|
||||
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", e);
|
||||
return;
|
||||
}
|
||||
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 class="custom-dash">-</span>]</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 (normalized).
|
||||
if (selectedFolder) {
|
||||
window.currentFolder = selectedFolder;
|
||||
} else {
|
||||
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
||||
}
|
||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||
|
||||
// Update file list title using breadcrumb.
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
if (window.currentFolder === "root") {
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
|
||||
} else {
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb(window.currentFolder) + ")";
|
||||
}
|
||||
// Bind breadcrumb events (click and drag/drop).
|
||||
bindBreadcrumbEvents();
|
||||
|
||||
// Load file list.
|
||||
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 in folder tree.
|
||||
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
||||
if (selectedEl) {
|
||||
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||
selectedEl.classList.add("selected");
|
||||
}
|
||||
|
||||
// Event binding for folder selection in folder tree.
|
||||
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);
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
if (selected === "root") {
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
|
||||
} else {
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb(selected) + ")";
|
||||
}
|
||||
// Re-bind breadcrumb events so the new breadcrumb is clickable.
|
||||
bindBreadcrumbEvents();
|
||||
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.innerHTML = "[" + '<span class="custom-dash">-</span>' + "]";
|
||||
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.innerHTML = "[" + '<span class="custom-dash">-</span>' + "]";
|
||||
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";
|
||||
});
|
||||
});
|
||||
101
getFileList.php
101
getFileList.php
@@ -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]);
|
||||
?>
|
||||
@@ -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);
|
||||
?>
|
||||
@@ -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;
|
||||
?>
|
||||
24
getUsers.php
24
getUsers.php
@@ -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);
|
||||
?>
|
||||
397
index.html
397
index.html
@@ -1,397 +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>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/resumable.js/1.1.0/resumable.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>
|
||||
<button id="changePasswordBtn" title="Change Password">
|
||||
<i class="material-icons">vpn_key</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>
|
||||
<div class="form-group remember-me-container">
|
||||
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
|
||||
<label for="rememberMeCheckbox">Remember me</label>
|
||||
</div>
|
||||
</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%;" novalidate>
|
||||
<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; position:relative;">
|
||||
<span>Drop files/folders here or click 'Choose Files'</span>
|
||||
<br />
|
||||
<!-- Note: Remove directory attributes so file picker only allows files -->
|
||||
<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">Choose Files</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 & 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>
|
||||
|
||||
<!-- Change Password-->
|
||||
<div id="changePasswordModal" class="modal" style="display:none;">
|
||||
<div class="modal-content" style="max-width:400px; margin:auto;">
|
||||
<span id="closeChangePasswordModal" style="cursor:pointer;">×</span>
|
||||
<h3>Change Password</h3>
|
||||
<input type="password" id="oldPassword" placeholder="Old Password" style="width:100%; margin: 5px 0;" />
|
||||
<input type="password" id="newPassword" placeholder="New Password" style="width:100%; margin: 5px 0;" />
|
||||
<input type="password" id="confirmPassword" placeholder="Confirm New Password"
|
||||
style="width:100%; margin: 5px 0;" />
|
||||
<button id="saveNewPasswordBtn" class="btn btn-primary" style="width:100%;">Save</button>
|
||||
</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="addUserPassword">Password:</label>
|
||||
<input type="password" id="addUserPassword" 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>
|
||||
19
logout.php
19
logout.php
@@ -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
155
main.js
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
158
moveFiles.php
158
moveFiles.php
@@ -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)]);
|
||||
}
|
||||
?>
|
||||
@@ -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
2599
openapi.json.dist
Normal file
File diff suppressed because it is too large
Load Diff
75
public/.htaccess
Normal file
75
public/.htaccess
Normal 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: short‑term 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
31
public/api.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
// public/api.php
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
|
||||
if (empty($_SESSION['authenticated'])) {
|
||||
header('Location: /index.html?redirect=/api.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['spec'])) {
|
||||
header('Content-Type: application/json');
|
||||
readfile(__DIR__ . '/../openapi.json.dist');
|
||||
exit;
|
||||
}
|
||||
|
||||
?><!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>FileRise API Docs</title>
|
||||
<script defer src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"
|
||||
integrity="sha384-4vOjrBu7SuDWXcAw1qFznVLA/sKL+0l4nn+J1HY8w7cpa6twQEYuh4b0Cwuo7CyX"
|
||||
crossorigin="anonymous"></script>
|
||||
<script defer src="/js/redoc-init.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="api.php?spec=1"></redoc>
|
||||
<div id="redoc-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
8
public/api/addUser.php
Normal file
8
public/api/addUser.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/addUser.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->addUser();
|
||||
8
public/api/admin/getConfig.php
Normal file
8
public/api/admin/getConfig.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/admin/getConfig.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||
|
||||
$adminController = new AdminController();
|
||||
$adminController->getConfig();
|
||||
63
public/api/admin/readMetadata.php
Normal file
63
public/api/admin/readMetadata.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
// public/api/admin/readMetadata.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
|
||||
// Only admins may read these
|
||||
if (empty($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Must supply ?file=share_links.json or share_folder_links.json
|
||||
if (empty($_GET['file'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing `file` parameter']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = basename($_GET['file']);
|
||||
$allowed = ['share_links.json', 'share_folder_links.json'];
|
||||
if (!in_array($file, $allowed, true)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Invalid file requested']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = META_DIR . $file;
|
||||
if (!file_exists($path)) {
|
||||
// Return empty object so JS sees `{}` not an error
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode((object)[]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$jsonData = file_get_contents($path);
|
||||
$data = json_decode($jsonData, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Corrupted JSON']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ——— Clean up expired entries ———
|
||||
$now = time();
|
||||
$changed = false;
|
||||
foreach ($data as $token => $entry) {
|
||||
if (!empty($entry['expires']) && $entry['expires'] < $now) {
|
||||
unset($data[$token]);
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
// overwrite file with cleaned data
|
||||
file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
// ——— Send cleaned data back ———
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
8
public/api/admin/updateConfig.php
Normal file
8
public/api/admin/updateConfig.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/admin/updateConfig.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||
|
||||
$adminController = new AdminController();
|
||||
$adminController->updateConfig();
|
||||
9
public/api/auth/auth.php
Normal file
9
public/api/auth/auth.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// public/api/auth/auth.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->auth();
|
||||
8
public/api/auth/checkAuth.php
Normal file
8
public/api/auth/checkAuth.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/auth/checkAuth.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->checkAuth();
|
||||
8
public/api/auth/login_basic.php
Normal file
8
public/api/auth/login_basic.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/auth/login_basic.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->loginBasic();
|
||||
8
public/api/auth/logout.php
Normal file
8
public/api/auth/logout.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/auth/logout.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->logout();
|
||||
8
public/api/auth/token.php
Normal file
8
public/api/auth/token.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/auth/token.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||
|
||||
$authController = new AuthController();
|
||||
$authController->getToken();
|
||||
8
public/api/changePassword.php
Normal file
8
public/api/changePassword.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/changePassword.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->changePassword();
|
||||
8
public/api/file/copyFiles.php
Normal file
8
public/api/file/copyFiles.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/copyFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->copyFiles();
|
||||
8
public/api/file/createShareLink.php
Normal file
8
public/api/file/createShareLink.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/createShareLink.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->createShareLink();
|
||||
8
public/api/file/deleteFiles.php
Normal file
8
public/api/file/deleteFiles.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/deleteFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->deleteFiles();
|
||||
6
public/api/file/deleteShareLink.php
Normal file
6
public/api/file/deleteShareLink.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->deleteShareLink();
|
||||
8
public/api/file/deleteTrashFiles.php
Normal file
8
public/api/file/deleteTrashFiles.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/deleteTrashFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->deleteTrashFiles();
|
||||
8
public/api/file/download.php
Normal file
8
public/api/file/download.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/download.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->downloadFile();
|
||||
8
public/api/file/downloadZip.php
Normal file
8
public/api/file/downloadZip.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/downloadZip.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->downloadZip();
|
||||
8
public/api/file/extractZip.php
Normal file
8
public/api/file/extractZip.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/extractZip.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->extractZip();
|
||||
8
public/api/file/getFileList.php
Normal file
8
public/api/file/getFileList.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/getFileList.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getFileList();
|
||||
8
public/api/file/getFileTag.php
Normal file
8
public/api/file/getFileTag.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/getFileTag.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getFileTags();
|
||||
6
public/api/file/getShareLinks.php
Normal file
6
public/api/file/getShareLinks.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getShareLinks();
|
||||
8
public/api/file/getTrashItems.php
Normal file
8
public/api/file/getTrashItems.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/getTrashItems.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->getTrashItems();
|
||||
8
public/api/file/moveFiles.php
Normal file
8
public/api/file/moveFiles.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/moveFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->moveFiles();
|
||||
8
public/api/file/renameFile.php
Normal file
8
public/api/file/renameFile.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/renameFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->renameFile();
|
||||
8
public/api/file/restoreFiles.php
Normal file
8
public/api/file/restoreFiles.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/restoreFiles.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->restoreFiles();
|
||||
8
public/api/file/saveFile.php
Normal file
8
public/api/file/saveFile.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/saveFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->saveFile();
|
||||
8
public/api/file/saveFileTag.php
Normal file
8
public/api/file/saveFileTag.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/saveFileTag.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->saveFileTag();
|
||||
8
public/api/file/share.php
Normal file
8
public/api/file/share.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/file/share.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->shareFile();
|
||||
8
public/api/folder/createFolder.php
Normal file
8
public/api/folder/createFolder.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/createFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->createFolder();
|
||||
8
public/api/folder/createShareFolderLink.php
Normal file
8
public/api/folder/createShareFolderLink.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/createShareFolderLink.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->createShareFolderLink();
|
||||
8
public/api/folder/deleteFolder.php
Normal file
8
public/api/folder/deleteFolder.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/deleteFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->deleteFolder();
|
||||
6
public/api/folder/deleteShareFolderLink.php
Normal file
6
public/api/folder/deleteShareFolderLink.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->deleteShareFolderLink();
|
||||
8
public/api/folder/downloadSharedFile.php
Normal file
8
public/api/folder/downloadSharedFile.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/downloadSharedFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->downloadSharedFile();
|
||||
8
public/api/folder/getFolderList.php
Normal file
8
public/api/folder/getFolderList.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/getFolderList.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->getFolderList();
|
||||
6
public/api/folder/getShareFolderLinks.php
Normal file
6
public/api/folder/getShareFolderLinks.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->getShareFolderLinks();
|
||||
8
public/api/folder/renameFolder.php
Normal file
8
public/api/folder/renameFolder.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/renameFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->renameFolder();
|
||||
8
public/api/folder/shareFolder.php
Normal file
8
public/api/folder/shareFolder.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/shareFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->shareFolder();
|
||||
8
public/api/folder/uploadToSharedFolder.php
Normal file
8
public/api/folder/uploadToSharedFolder.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/folder/uploadToSharedFolder.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$folderController = new FolderController();
|
||||
$folderController->uploadToSharedFolder();
|
||||
8
public/api/getUserPermissions.php
Normal file
8
public/api/getUserPermissions.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/getUserPermissions.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->getUserPermissions();
|
||||
8
public/api/getUsers.php
Normal file
8
public/api/getUsers.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/getUsers.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->getUsers(); // This will output the JSON response
|
||||
8
public/api/removeUser.php
Normal file
8
public/api/removeUser.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/removeUser.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->removeUser();
|
||||
9
public/api/totp_disable.php
Normal file
9
public/api/totp_disable.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// public/api/totp_disable.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->disableTOTP();
|
||||
8
public/api/totp_recover.php
Normal file
8
public/api/totp_recover.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/totp_recover.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->recoverTOTP();
|
||||
8
public/api/totp_saveCode.php
Normal file
8
public/api/totp_saveCode.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/totp_saveCode.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->saveTOTPRecoveryCode();
|
||||
9
public/api/totp_setup.php
Normal file
9
public/api/totp_setup.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// public/api/totp_setup.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->setupTOTP();
|
||||
9
public/api/totp_verify.php
Normal file
9
public/api/totp_verify.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// public/api/totp_verify.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->verifyTOTP();
|
||||
8
public/api/updateUserPanel.php
Normal file
8
public/api/updateUserPanel.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/updateUserPanel.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->updateUserPanel();
|
||||
8
public/api/updateUserPermissions.php
Normal file
8
public/api/updateUserPermissions.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/updateUserPermissions.php
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->updateUserPermissions();
|
||||
8
public/api/upload/removeChunks.php
Normal file
8
public/api/upload/removeChunks.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// public/api/upload/removeChunks.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UploadController.php';
|
||||
|
||||
$uploadController = new UploadController();
|
||||
$uploadController->removeChunks();
|
||||
7
public/api/upload/upload.php
Normal file
7
public/api/upload/upload.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
// public/api/upload/upload.php
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UploadController.php';
|
||||
|
||||
$uploadController = new UploadController();
|
||||
$uploadController->handleUpload();
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -25,22 +25,15 @@ body {
|
||||
/* CONTAINER */
|
||||
.container,
|
||||
.container-fluid {
|
||||
margin-top: 20px;
|
||||
margin-top: 10px;
|
||||
padding-right: 4px !important;
|
||||
padding-left: 4px !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (min-width: 1300px) {
|
||||
.container-fluid {
|
||||
padding-left: 50px !important;
|
||||
padding-right: 50px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.container-fluid {
|
||||
padding-left: 100px !important;
|
||||
padding-right: 100px !important;
|
||||
padding-left: 30px !important;
|
||||
padding-right: 30px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,12 +45,6 @@ body {
|
||||
/* FLEXBOX HEADER: LOGO, TITLE, BUTTONS FIXED */
|
||||
/************************************************************/
|
||||
|
||||
|
||||
#uploadCard,
|
||||
#folderManagementCard {
|
||||
min-height: 342px;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -82,7 +69,7 @@ body {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
height: 55px;
|
||||
padding: 10px 20px;
|
||||
background-color: #2196F3;
|
||||
transition: background-color 0.3s ease;
|
||||
@@ -93,30 +80,21 @@ body.dark-mode .header-container {
|
||||
background-color: #1f1f1f;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
#darkModeIcon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
max-height: 70px;
|
||||
max-height: 50px;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-logo svg {
|
||||
height: 70px;
|
||||
height: 50px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
padding: 0 20px;
|
||||
background-color: #2196F3;
|
||||
transition: background-color 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
body.dark-mode header {
|
||||
background-color: #1f1f1f;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
|
||||
@@ -216,6 +194,7 @@ body.dark-mode header {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Folder Help Tooltip - Light Mode */
|
||||
.folder-help-tooltip {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
@@ -225,6 +204,7 @@ body.dark-mode header {
|
||||
box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Folder Help Tooltip - Dark Mode */
|
||||
body.dark-mode .folder-help-tooltip {
|
||||
background-color: #333 !important;
|
||||
color: #eee !important;
|
||||
@@ -234,17 +214,19 @@ body.dark-mode .folder-help-tooltip {
|
||||
#folderHelpBtn i.material-icons.folder-help-icon {
|
||||
-webkit-text-fill-color: orange !important;
|
||||
color: inherit !important;
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
|
||||
body.dark-mode #folderHelpBtn i.material-icons.folder-help-icon {
|
||||
-webkit-text-fill-color: #ffa500 !important;
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
|
||||
/************************************************************/
|
||||
/* RESPONSIVE HEADER FIXES */
|
||||
/************************************************************/
|
||||
|
||||
@media (max-width: 970px) {
|
||||
@media (max-width: 790px) {
|
||||
.header-container {
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
@@ -284,7 +266,7 @@ body.dark-mode #folderHelpBtn i.material-icons.folder-help-icon {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
margin-top: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,21 +359,6 @@ body.dark-mode #loginForm button:hover {
|
||||
CARDS & MODALS
|
||||
=========================================================== */
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
border: 1px solid #ddd;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
body.dark-mode .card {
|
||||
background-color: #2c2c2c;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
#restoreFilesModal .modal-content {
|
||||
position: fixed !important;
|
||||
top: 50% !important;
|
||||
@@ -674,12 +641,15 @@ body.dark-mode .editor-header {
|
||||
}
|
||||
|
||||
#uploadBtn {
|
||||
margin-top: 20px;
|
||||
font-size: 20px;
|
||||
padding: 10px 22px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-body.d-flex.flex-column {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
|
||||
#customChooseBtn {
|
||||
background-color: #9E9E9E;
|
||||
color: #fff;
|
||||
@@ -713,12 +683,25 @@ body.dark-mode .editor-header {
|
||||
|
||||
.material-icons.pauseResumeBtn {
|
||||
color: black !important;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
body.dark-mode .material-icons.pauseResumeBtn {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
body.dark-mode .material-icons.pauseResumeBtn:hover {
|
||||
background-color: rgba(255, 215, 0, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
body:not(.dark-mode) .material-icons.pauseResumeBtn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#uploadProgressContainer ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
@@ -860,7 +843,8 @@ body.dark-mode .material-icons.pauseResumeBtn {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rename-btn .material-icons {
|
||||
.rename-btn .material-icons,
|
||||
#renameFolderBtn .material-icons {
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
@@ -1008,29 +992,6 @@ label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#uploadFolderRow {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#uploadFolderRow .col-md-6 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#uploadFolderRow .col-md-6:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card-body .form-group {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
|
||||
#createFolderBtn {
|
||||
margin-top: 0px !important;
|
||||
height: 40px !important;
|
||||
@@ -1085,37 +1046,9 @@ body.dark-mode .custom-prev-next-btn:hover:not(:disabled) {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.folder-option:hover {
|
||||
background-color: #f0f0f0;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.folder-option.selected {
|
||||
background-color: #d0d0d0;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
body.dark-mode .folder-option.selected {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
body.dark-mode .folder-option:hover {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.custom-folder-card-body {
|
||||
padding-top: 5px !important;
|
||||
}
|
||||
|
||||
#customToast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
@@ -1130,7 +1063,7 @@ body.dark-mode .folder-option:hover {
|
||||
}
|
||||
|
||||
#customToast.show {
|
||||
opacity: 1;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.button-wrap {
|
||||
@@ -1160,8 +1093,10 @@ body.dark-mode .folder-option:hover {
|
||||
|
||||
#fileListContainer {
|
||||
max-width: 100%;
|
||||
padding: 10px 5px;
|
||||
margin: 20px auto;
|
||||
padding-bottom: 10px !important;
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 750px) {
|
||||
@@ -1175,11 +1110,6 @@ body.dark-mode #fileListContainer {
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
padding-top: 10px !important;
|
||||
padding-bottom: 10px !important;
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#fileListContainer>h2,
|
||||
@@ -1209,6 +1139,7 @@ body.dark-mode #fileListContainer {
|
||||
|
||||
#fileListTitle {
|
||||
font-size: 1.8em;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@@ -1269,6 +1200,7 @@ body.dark-mode #fileListContainer {
|
||||
/* ===========================================================
|
||||
FOLDER TREE STYLES
|
||||
=========================================================== */
|
||||
/* Make breadcrumb links look clickable */
|
||||
.breadcrumb-link {
|
||||
cursor: pointer;
|
||||
color: #007bff;
|
||||
@@ -1318,15 +1250,38 @@ body.dark-mode #fileListContainer {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.folder-option {
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
#folderTreeContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.folder-option {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.folder-option:hover {
|
||||
background-color: #f0f0f0;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.folder-option.selected {
|
||||
background-color: #d0d0d0;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
body.dark-mode .folder-option.selected {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
body.dark-mode .folder-option:hover {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
FILE MANAGER INLINE STYLE REMOVAL - New Classes
|
||||
=========================================================== */
|
||||
@@ -1385,7 +1340,6 @@ body.dark-mode .image-preview-modal-content {
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
/* Your custom styles here */
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
@@ -1621,39 +1575,6 @@ body.dark-mode .btn-secondary {
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
#toggleViewBtn {
|
||||
margin-bottom: 20px;
|
||||
margin-left: 14px;
|
||||
padding: 10px 20px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
transition: background 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#toggleViewBtn {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
#toggleViewBtn {
|
||||
margin-left: 0 !important;
|
||||
margin-right: auto !important;
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
#toggleViewBtn:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
body.dark-mode .btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: #fff;
|
||||
@@ -1766,21 +1687,6 @@ body.dark-mode .folder-help-icon {
|
||||
|
||||
}
|
||||
|
||||
body.dark-mode #searchIcon {
|
||||
background-color: #444;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
body.dark-mode #searchInput {
|
||||
background-color: #333;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
|
||||
body.dark-mode .CodeMirror {
|
||||
background: #1e1e1e !important;
|
||||
color: #ffffff !important;
|
||||
@@ -1808,6 +1714,20 @@ body.dark-mode .CodeMirror-matchingbracket {
|
||||
border-bottom: 1px solid #ffffff !important;
|
||||
}
|
||||
|
||||
.zoom_in,
|
||||
.zoom_out,
|
||||
.rotate_left,
|
||||
.rotate_right {
|
||||
background: rgba(80, 80, 80, 0.6) !important;
|
||||
border: none !important;
|
||||
color: white !important;
|
||||
cursor: pointer !important;
|
||||
border-radius: 4px !important;
|
||||
transition: background 0.3s ease, box-shadow 0.3s ease !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3) !important;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
.gallery-nav-btn {
|
||||
background: rgba(80, 80, 80, 0.6) !important;
|
||||
border: none !important;
|
||||
@@ -1820,21 +1740,15 @@ body.dark-mode .CodeMirror-matchingbracket {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.gallery-nav-btn:hover {
|
||||
.gallery-nav-btn:hover,
|
||||
.zoom_in:hover,
|
||||
.zoom_out:hover,
|
||||
.rotate_left:hover,
|
||||
.rotate_right:hover {
|
||||
background: rgba(80, 80, 80, 0.8) !important;
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
.gallery-nav-btn.left {
|
||||
left: 10px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.gallery-nav-btn.right {
|
||||
right: 10px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.drop-hover {
|
||||
background-color: #e0e0e0;
|
||||
border: 1px dashed #666;
|
||||
@@ -1858,4 +1772,397 @@ body.dark-mode .drop-hover {
|
||||
|
||||
#restoreFilesList li label {
|
||||
margin-left: 8px !important;
|
||||
}
|
||||
|
||||
body.dark-mode #fileContextMenu {
|
||||
background-color: #2c2c2c !important;
|
||||
border: 1px solid #555 !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
body.dark-mode #fileContextMenu div {
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
#folderContextMenu {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
body.dark-mode #folderContextMenu {
|
||||
background-color: #2c2c2c;
|
||||
border-color: #555;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.drop-target-sidebar {
|
||||
display: none;
|
||||
width: 50px;
|
||||
transition: width 0.3s ease;
|
||||
background-color: #f8f9fa;
|
||||
border-right: 2px dashed #1565C0;
|
||||
padding: 10px;
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.drop-target-sidebar {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.drop-target-sidebar.active {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.main-column {
|
||||
flex: 1;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
#uploadFolderRow {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
#uploadFolderRow {
|
||||
gap: 0px;
|
||||
}
|
||||
}
|
||||
#leftCol,
|
||||
#rightCol {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-width: 370px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
#leftCol {
|
||||
flex: 0 1 60%;
|
||||
}
|
||||
#rightCol {
|
||||
flex: 0 1 40%;
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
.drop-target-sidebar {
|
||||
display: none !important;
|
||||
}
|
||||
#uploadFolderRow {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#leftCol, #rightCol {
|
||||
flex: 0 1 100% !important;
|
||||
}
|
||||
#rightCol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#sidebarDropArea.highlight,
|
||||
#uploadFolderRow.highlight {
|
||||
border: 2px dashed #1565C0;
|
||||
background-color: #eef;
|
||||
}
|
||||
|
||||
.drag-header {
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.drag-header::after {
|
||||
content: '⋮⋮';
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 16px;
|
||||
color: #1565C0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dragging {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 20px 30px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
#uploadCard,
|
||||
#folderManagementCard {
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
|
||||
#uploadFolderRow.highlight {
|
||||
min-height: 320px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#sidebarDropArea,
|
||||
#uploadFolderRow {
|
||||
background-color: transparent;
|
||||
}
|
||||
#sidebarDropArea {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.dark-mode #sidebarDropArea,
|
||||
body.dark-mode #uploadFolderRow {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
body.dark-mode #sidebarDropArea.highlight,
|
||||
body.dark-mode #uploadFolderRow.highlight {
|
||||
background-color: #333;
|
||||
border: 2px dashed #555;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.drop-target-sidebar.highlight {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.drop-target-sidebar:not(.highlight) {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.dragging:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#sidebarDropArea > .card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
border: 1px solid #ddd;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
body.dark-mode .card {
|
||||
background-color: #2c2c2c;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.custom-folder-card-body {
|
||||
padding-top: 5px !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
#addUserModal,
|
||||
#removeUserModal {
|
||||
z-index: 5000 !important;
|
||||
}
|
||||
|
||||
#customConfirmModal {
|
||||
z-index: 6000 !important;
|
||||
}
|
||||
|
||||
.admin-panel-content {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
body.dark-mode .admin-panel-content {
|
||||
background: #2c2c2c;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
body.dark-mode .admin-panel-content input,
|
||||
body.dark-mode .admin-panel-content select,
|
||||
body.dark-mode .admin-panel-content textarea {
|
||||
background: #3a3a3a;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
body.dark-mode .admin-panel-content label {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
#openChangePasswordModalBtn {
|
||||
width: auto;
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
margin-right: 300px;
|
||||
}
|
||||
|
||||
#changePasswordModal {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.download-spinner {
|
||||
font-size: 48px;
|
||||
animation: spin 2s linear infinite;
|
||||
color: var(--download-spinner-color, #000);
|
||||
}
|
||||
|
||||
body:not(.dark-mode) {
|
||||
--download-spinner-color: #000;
|
||||
}
|
||||
|
||||
body.dark-mode {
|
||||
--download-spinner-color: #fff;
|
||||
}
|
||||
|
||||
.rise-effect {
|
||||
transform: translateY(-20px);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-modal-btn,
|
||||
.collapse-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
font-size: 24px;
|
||||
color: #616161;
|
||||
border-radius: 50%;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-modal-btn:hover,
|
||||
.collapse-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.toggle-modal-btn:focus,
|
||||
.collapse-btn:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.header-drop-zone {
|
||||
width: 66px;
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.header-drop-zone.drag-active {
|
||||
border: 2px dashed #1565C0;
|
||||
background-color: #eef;
|
||||
background-color: transparent;
|
||||
transition: width 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body.dark-mode .header-drop-zone.drag-active {
|
||||
background-color: #333;
|
||||
border: 2px dashed #555;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-drop-zone.drag-active:empty::before {
|
||||
content: "Drop";
|
||||
font-size: 10px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
/* Disable text selection on rows to prevent accidental copying when shift-clicking */
|
||||
#fileList tbody tr.clickable-row {
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE10+/Edge */
|
||||
user-select: none; /* Standard */
|
||||
}
|
||||
|
||||
#fileSummary {
|
||||
color: black;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
#fileSummary {
|
||||
float: none !important;
|
||||
margin: 0 auto !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
body.dark-mode #fileSummary {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#searchIcon {
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
body.dark-mode #searchIcon {
|
||||
background-color: #444;
|
||||
border: 1px solid #555;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
body.dark-mode #searchInput {
|
||||
background-color: #333;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 6px 8px;
|
||||
margin: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.btn-icon .material-icons,
|
||||
#searchIcon .material-icons {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-icon:hover,
|
||||
.btn-icon:focus {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body.dark-mode .btn-icon .material-icons,
|
||||
body.dark-mode #searchIcon .material-icons {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
body.dark-mode .btn-icon:hover,
|
||||
body.dark-mode .btn-icon:focus {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user