Compare commits

...
5 Commits
Author SHA1 Message Date
Michal Sedlák 99b1c96f4b revise backend 2026-10-18 15:55:34 +02:00
Michal Sedlák dc452059f3 Fix SFTP root navigation on Windows
Use portable POSIX parent paths instead of platform-dependent dirname() for remote navigation, previews, uploads, file operations and shared URLs.

Normalize browser paths, avoid doubled root separators and preserve top-level directory names in ZIP downloads.

Add regression coverage for root navigation, generated links, relative paths and ZIP members, and document the new test command. Verified with PHP syntax checks and all three regression suites.
2026-10-16 10:36:57 +02:00
Michal Sedlák 574662c1cc stuff 2026-10-16 10:31:29 +02:00
Michal Sedlák 295a9aca07 Document project setup and database installation
Expand README with application features, requirements, XAMPP setup, connection configuration, project structure and troubleshooting.

Add Czech database documentation covering SQL import, manual SQL and phpMyAdmin table setup, bcrypt administrator creation, database permissions and verification.

Provide the missing share table schema as a separate SQL supplement and explain its relationship to the original users dump.
2026-10-16 10:29:25 +02:00
Michal Sedlák 565fd18379 Redesign SFTP workspace and fix server monitoring
Replace Bootstrap dependencies with custom blue-accented light and dark themes and native dialogs.

Share the sidebar, top bar and responsive layout across file browsing, account settings, user management, server statistics and file previews. Add folder search, type filters, upload progress and asset cache versioning.

Fix SSH configuration usage, filesystem discovery, missing statistics and RAID health parsing. Adapt PDF sizing and fullscreen controls to the shared workspace.

Validation: PHP workspace and server statistics regression tests, JavaScript syntax checks, responsive browser checks and live XAMPP navigation.
2026-10-16 10:20:55 +02:00
44 changed files with 2253 additions and 2705 deletions
+4
View File
@@ -0,0 +1,4 @@
## Architecture
- Frontend: html + JavaScript
- Backend: PHP
- DB: MySQL with phpmyadmin
+281
View File
@@ -0,0 +1,281 @@
# Nastavení databáze
Tento návod popisuje databázi webové aplikace USB RAID Array pro MySQL/MariaDB. Lze použít phpMyAdmin v XAMPP nebo příkazový klient `mysql`.
Vyber jednu ze dvou cest: [import SQL](#varianta-a-import-sql) nebo [ruční vytvoření tabulek](#varianta-b-ruční-vytvoření-tabulek). Poté pokračuj [vytvořením prvního správce](#první-administrátor) a [nastavením připojení](#připojení-aplikace).
## Co aplikace ukládá
| Tabulka | Účel | Používají ji |
| --- | --- | --- |
| `users` | Webové účty, hashe hesel, role, vzdálené cesty a oprávnění. | `login.php`, `adminpanel.php`, `changepassword.php` |
| `share` | Tokeny sdílených médií, cesta k souboru, expirace a povolení stažení. | `ftp/view.php`, `ftp/shared.php` |
Obsah souborů zůstává na SFTP serveru. Databáze uchovává jen účty a metadata sdílení.
### Původní SQL soubor a doplnění
[usbraidlogin.sql](usbraidlogin.sql) je původní dump z phpMyAdmin. Obsahuje vytvoření tabulky `users` a vložení uživatelských dat. **Neobsahuje `CREATE DATABASE`, `USE` ani tabulku `share`.** Tabulka `users` v tomto dumpu nemá primární ani unikátní klíč.
[share.sql](share.sql) doplňuje strukturu, kterou aktuální kód potřebuje pro sdílené odkazy. Je odvozená z dotazů v `view.php` a `shared.php`, nikoli z původního dumpu. Neobsahuje testovací odkazy ani uživatelská data. `IF NOT EXISTS` při opakovaném spuštění nevytvoří druhou tabulku, ale také neopraví již existující nekompatibilní schéma.
Obě varianty níže používají databázi `usbraidlogin`, InnoDB a `utf8mb4_unicode_ci` podle původního SQL. Import původního dumpu prováděj do nové databáze; na existující tabulce `users` by `CREATE TABLE` skončilo chybou.
## Varianta A: import SQL
### Přes phpMyAdmin
1. V XAMPP spusť Apache a MySQL.
2. Otevři `http://localhost/phpmyadmin/` a přihlas se databázovým účtem s právem vytvářet databáze a tabulky.
3. Na kartě **SQL** vytvoř databázi:
```sql
CREATE DATABASE `usbraidlogin`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
```
4. V levém panelu vyber **usbraidlogin**. Tento krok je nutný, protože dump neobsahuje příkaz `USE`.
5. Otevři **Import**, vyber `Documentation/usbraidlogin.sql`, ponech formát SQL a spusť import.
6. Ve stejné databázi znovu otevři **Import** a importuj `Documentation/share.sql`.
7. Zkontroluj, že jsou přítomné tabulky `users` a `share`.
Původní dump vkládá i účty s hashi hesel. Z hashe nelze zjistit původní heslo. Pro vlastní instalaci vytvoř vlastní účet podle kapitoly [První administrátor](#první-administrátor). Importované účty před zpřístupněním instalace zkontroluj a ponech jen ty, které chceš používat.
### Přes příkazový řádek
V PowerShellu otevři klienta z XAMPP:
```powershell
& 'C:\xampp\mysql\bin\mysql.exe' -u root -p
```
Použij jméno svého správce databáze místo `root`, pokud se liší. Heslo se zadává až do výzvy klienta. Jestli má lokální účet prázdné heslo, potvrď výzvu Enterem.
Na Linuxu nebo pokud je klient v `PATH`:
```sh
mysql -u root -p
```
V klientovi spusť následující příkazy. Cesty uprav podle umístění repozitáře; v příkladu pro Windows jsou použité dopředné lomítka:
```sql
CREATE DATABASE `usbraidlogin`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE `usbraidlogin`;
SOURCE C:/cesta/USB-RAID-Array/Documentation/usbraidlogin.sql;
SOURCE C:/cesta/USB-RAID-Array/Documentation/share.sql;
SHOW TABLES;
```
`SOURCE` je příkaz klienta `mysql`. V phpMyAdmin používej kartu Import. Na Linuxu nahraď cesty například `/cesta/USB-RAID-Array/Documentation/usbraidlogin.sql` a odpovídající cestou k `share.sql`.
## Varianta B: ruční vytvoření tabulek
Tato varianta vytvoří prázdné tabulky bez importovaných účtů. Následující SQL vlož do karty **SQL** v phpMyAdmin nebo do klienta `mysql`. Původní dump už v této variantě neimportuj.
### 1. Databáze
```sql
CREATE DATABASE `usbraidlogin`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE `usbraidlogin`;
```
Pokud databáze už existuje, vyber ji příkazem `USE` a přeskoč `CREATE DATABASE`. Před vytvořením tabulek ověř jejich existenci pomocí `SHOW TABLES`; existující tabulky nemaž kvůli opakování návodu.
### 2. Tabulka `users`
Struktura přesně odpovídá tabulce v původním `usbraidlogin.sql`:
```sql
CREATE TABLE `users` (
`uname` varchar(255) NOT NULL,
`pswd` varchar(60) NOT NULL,
`admin` tinyint(1) NOT NULL DEFAULT '0',
`defPath` varchar(255) NOT NULL DEFAULT '/',
`delPer` tinyint(1) NOT NULL DEFAULT '0',
`downPer` tinyint(1) NOT NULL DEFAULT '0',
`upPer` tinyint(1) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
| Sloupec | Význam |
| --- | --- |
| `uname` | Přihlašovací jméno. Každý účet vytvářej s odlišným jménem. |
| `pswd` | Bcrypt hash hesla vytvořený PHP, dlouhý 60 znaků. |
| `admin` | `1` pro správce, `0` pro běžný účet; správce má přístup ke správě účtů a statistikám serveru. |
| `defPath` | Absolutní vzdálená cesta na SFTP serveru, například `/mnt/raid`. |
| `delPer` | `1` povoluje mazání souborů a adresářů. |
| `downPer` | `1` povoluje stahování v rozhraní. |
| `upPer` | `1` povoluje nahrávání a úpravy, například vytváření, přejmenování, editaci a rozbalování archivů. |
Hodnoty oprávnění zadávej jako `0` nebo `1`. Role správce a souborová oprávnění jsou samostatné sloupce; pro plný přístup nastav prvnímu správci všechny čtyři přepínače na `1`.
Věrná kopie původního schématu nemá unikátní index pro `uname`. Administrace duplicity ověřuje při přidávání účtu, ale při ručním SQL je musíš zkontrolovat sám.
### 3. Tabulka `share`
Stejnou tabulku vytváří doplňující soubor `share.sql`:
```sql
CREATE TABLE IF NOT EXISTS `share` (
`token` char(32) NOT NULL,
`file_path` text NOT NULL,
`expiration` datetime NOT NULL,
`download_allowed` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`token`),
KEY `idx_share_expiration` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
| Sloupec | Význam |
| --- | --- |
| `token` | Identifikátor odkazu v URL; aplikace generuje 32 hexadecimálních znaků pomocí `bin2hex(random_bytes(16))`. |
| `file_path` | Vzdálená cesta ke sdílenému souboru. |
| `expiration` | Datum a čas konce platnosti. Dotaz v `shared.php` vyžaduje `expiration > NOW()`. |
| `download_allowed` | `1` povoluje stažení přes sdílený odkaz, `0` pouze náhled. |
Primární klíč zajišťuje jedinečnost tokenu, index `idx_share_expiration` usnadňuje dotazy podle expirace. Mezi tabulkami není cizí klíč: současný kód u odkazu neukládá autora ani ID uživatele.
### Alternativa: naklikání tabulek v phpMyAdmin
Místo spuštění uvedených `CREATE TABLE` můžeš stejné tabulky vytvořit přes formulář:
1. Vyber databázi `usbraidlogin`, zvol vytvoření nové tabulky `users` a počet sloupců **7**.
2. Zadej názvy a datové typy podle SQL výše: `uname VARCHAR(255)`, `pswd VARCHAR(60)`, `admin TINYINT(1)`, `defPath VARCHAR(255)`, `delPer TINYINT(1)`, `downPer TINYINT(1)` a `upPer TINYINT(1)`.
3. U všech sloupců ponech **NULL vypnuté** a nezapínej `A_I` (AUTO_INCREMENT). Přepínačům nastav výchozí hodnotu `0`, sloupci `defPath` hodnotu `/`. `uname` a `pswd` nemají výchozí hodnotu. Pro shodu s původním dumpem nenastavuj žádný primární klíč.
4. Vyber engine **InnoDB**, porovnávání **utf8mb4_unicode_ci** a tabulku ulož.
5. Vytvoř tabulku `share` se **4** sloupci: `token CHAR(32)`, `file_path TEXT`, `expiration DATETIME` a `download_allowed TINYINT(1)`.
6. Opět vypni NULL a AUTO_INCREMENT. U `download_allowed` nastav výchozí hodnotu `0`; ostatní sloupce výchozí hodnotu nemají. Sloupci `token` nastav index **PRIMARY**.
7. Použij InnoDB a utf8mb4_unicode_ci. Po uložení na kartě **Struktura** přidej běžný index `idx_share_expiration` na sloupci `expiration`.
8. Výsledek porovnej pomocí `SHOW CREATE TABLE users;` a `SHOW CREATE TABLE share;` s uvedenými SQL definicemi.
## První administrátor
### 1. Vytvoření hashe hesla
Heslo do `pswd` neukládej jako prostý text ani jako MD5/SHA hash. Přihlášení používá PHP `password_verify()` a aplikace vytváří hesla pomocí `PASSWORD_BCRYPT`.
Pokud je PHP v `PATH`, spusť:
```sh
php -r "echo password_hash('ZVOLTE_VLASTNI_HESLO', PASSWORD_BCRYPT), PHP_EOL;"
```
V PowerShellu s XAMPP lze použít plnou cestu:
```powershell
& 'C:\xampp\php\php.exe' -r "echo password_hash('ZVOLTE_VLASTNI_HESLO', PASSWORD_BCRYPT), PHP_EOL;"
```
`ZVOLTE_VLASTNI_HESLO` před spuštěním nahraď vlastním heslem. Zvol alespoň 8 znaků, jednu číslici a jedno velké písmeno, aby odpovídalo pravidlům správy účtů. Výstup bude bcrypt hash začínající například `$2y$10$`. Každé spuštění může pro stejné heslo vytvořit jiný platný hash.
Pokud nechceš heslo uvést v historii příkazové řádky, spusť `php -a` a zavolej `password_hash()` v interaktivním PHP shellu. Příkaz s vlastním heslem ani jeho výstup neukládej do veřejných ukázek dokumentace.
### 2. Vložení účtu
Nejprve ověř, zda zvolené jméno už není použité:
```sql
USE `usbraidlogin`;
SELECT `uname`, `admin`, `defPath` FROM `users` WHERE `uname` = 'spravce';
```
Pokud dotaz nic nevrátí, vlož účet. Text `SEM_VLOZTE_BCRYPT_HASH` nahraď celým skutečným hashem z předchozího kroku a `/mnt/raid` skutečnou cestou na SFTP serveru:
```sql
INSERT INTO `users`
(`uname`, `pswd`, `admin`, `defPath`, `delPer`, `downPer`, `upPer`)
VALUES
('spravce', 'SEM_VLOZTE_BCRYPT_HASH', 1, '/mnt/raid', 1, 1, 1);
```
Tento SQL příkaz je šablona — placeholder není platný hash. Do přihlašovacího formuláře následně zadávej jméno `spravce` a původní heslo, ze kterého jsi hash vytvořil. Další účty už lze přidávat přes **User management**.
Pokud jméno existuje, další stejné nevkládej. Pro obnovení přístupu ke svému účtu lze v phpMyAdmin nahradit pouze jeho hash:
```sql
UPDATE `users`
SET `pswd` = 'SEM_VLOZTE_BCRYPT_HASH'
WHERE `uname` = 'spravce';
```
## Připojení aplikace
Pro aplikaci vytvoř samostatný databázový účet. Následující příklad předpokládá, že se PHP připojuje k databázi lokálně jako `usbraid_app` na hostiteli `localhost`:
```sql
CREATE USER 'usbraid_app'@'localhost' IDENTIFIED BY 'DOPLNTE_VLASTNI_HESLO_DB';
GRANT SELECT, INSERT, UPDATE, DELETE ON `usbraidlogin`.* TO 'usbraid_app'@'localhost';
```
DDL příkazy pro vytvoření tabulek spouštěj správcovským databázovým účtem. Aplikace pro běžný provoz nepotřebuje práva `CREATE` ani `DROP`. Pokud účet již existuje, `CREATE USER` neopakuj; nastav mu potřebná práva. Pro vzdálenou databázi použij hostitele odpovídajícího umístění PHP serveru místo `localhost`.
V existujícím `Web/config/config.php` nastav:
```php
$DB_HOST = 'localhost';
$DB_USER = 'usbraid_app';
$DB_PASS = 'DOPLNTE_VLASTNI_HESLO_DB';
$DB_NAME = 'usbraidlogin';
```
Účet MySQL, webový účet z tabulky `users` a SFTP/SSH účet jsou tři různé věci. Změna hesla ve webové aplikaci mění pouze hash v `users.pswd`.
### Časové pásmo sdílených odkazů
`view.php` počítá expiraci v `Europe/Prague`, zatímco databázový dotaz ji porovnává s `NOW()`. Databázový čas musí odpovídat tomuto pásmu, jinak může odkaz vypršet dříve nebo později. Ověř:
```sql
SELECT NOW() AS database_time,
@@global.time_zone AS global_time_zone,
@@session.time_zone AS session_time_zone,
@@system_time_zone AS system_time_zone;
```
Pokud databáze používá `SYSTEM`, ověř časové pásmo systému databázového serveru. U spravované instalace lze nakonfigurovat časové pásmo MySQL na `Europe/Prague`, pokud má server načtené tabulky časových pásem. Jednorázové `SET time_zone` v phpMyAdmin mění pouze jeho spojení a nevyřeší nastavení nových spojení aplikace. Pevný posun `+01:00` nepokrývá český letní čas.
## Ověření instalace
V databázi spusť:
```sql
USE `usbraidlogin`;
SHOW TABLES;
DESCRIBE `users`;
DESCRIBE `share`;
SELECT `uname`, `admin`, `defPath`, `delPer`, `downPer`, `upPer`,
CHAR_LENGTH(`pswd`) AS hash_length
FROM `users`;
```
Výsledek má obsahovat obě tabulky. U účtu vytvořeného podle návodu má mít `hash_length` hodnotu `60`. Délka sama o sobě ještě nepotvrzuje správnost hesla; tu ověří přihlášení.
V aplikaci pak ověř:
1. Přihlášení vlastním administrátorem a otevření správy účtů.
2. Správnou výchozí cestu a přístup k SFTP souborům.
3. Vytvoření sdíleného odkazu u testovacího média a jeho otevření v anonymním okně.
4. Platnost odkazu a chování volby stažení.
Změny role, cesty a oprávnění provedené přímo v databázi se do existujícího webového přihlášení nepromítnou okamžitě — hodnoty se načítají do session při přihlášení. Po takové změně se odhlas a znovu přihlas.
## Řešení chyb
| Chyba | Příčina a řešení |
| --- | --- |
| `Unknown database 'usbraidlogin'` | Vytvoř databázi a zkontroluj `$DB_NAME`. |
| `No database selected` při importu | Před importem vyber databázi v phpMyAdmin nebo spusť `USE usbraidlogin`. |
| `Table 'users' already exists` | Dump se importuje do již vytvořeného schématu. Zkontroluj existující tabulku a nepokračuj slepým mazáním dat. |
| `Table 'usbraidlogin.share' doesn't exist` | Importuj `share.sql` nebo tabulku vytvoř ručně podle návodu. |
| `Access denied for user` | Zkontroluj `$DB_USER`, `$DB_PASS`, hostitele a přidělená práva. |
| Přihlášení hlásí nesprávné údaje | Ověř jméno a bcrypt hash; do formuláře patří původní heslo. |
| Sdílený odkaz ihned expiruje | Zkontroluj časové pásmo a `NOW()` na databázovém serveru. |
| Čeština se ukládá nesprávně | Tabulky používají `utf8mb4`; současný helper v `config.php` nastavuje spojení na `utf8`. Pro plné čtyřbajtové znaky, například emoji, je potřeba sjednotit i spojení na `utf8mb4`. |
+12
View File
@@ -0,0 +1,12 @@
-- Doplneni puvodniho usbraidlogin.sql o tabulku pro sdilene odkazy.
-- Nejprve vyberte databazi usbraidlogin v phpMyAdmin nebo prikazem USE.
-- Struktura odpovida INSERT/SELECT v Web/content/ftp/view.php a shared.php.
CREATE TABLE IF NOT EXISTS `share` (
`token` char(32) NOT NULL,
`file_path` text NOT NULL,
`expiration` datetime NOT NULL,
`download_allowed` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`token`),
KEY `idx_share_expiration` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+160 -2
View File
@@ -1,2 +1,160 @@
# USB-RAID-pole
Cílem práce je nalézt využití zastaralých paměťových médií jako například slabopaměťové USB flash disky s malou paměťovou kapacitou. Technologii RAID (NAS) pole v praxi implementujte na vícero RAID technologiích a vzájemně komparujte řešení. Komparujte chování navrženého systému minimálně na technologiích RAID 0, RAID 1 a RAID 5. Vytvořte vzdálené připojení na toto RAID pole s možností nahrání a stažení souborů po přihlášení uživatele (web login). K tomuto účelu vytvořte webovou aplikaci. Zabezpečte vzdálené připojení. Použité technologie: RAID, NAS, FTP, USB, Flash, HTML, PHP, MySQL.
# USB RAID Array
Webový SFTP klient pro vzdálenou práci se soubory na RAID/NAS úložišti. Projekt vznikl jako školní práce zaměřená na využití starších USB flash disků a porovnání RAID 0, RAID 1 a RAID 5.
Aplikace běží na PHP serveru, účty a sdílené odkazy ukládá do MySQL/MariaDB a k úložišti se připojuje přes SFTP. Statistiky vzdáleného Linux serveru získává přes SSH. Samotné vytvoření RAID pole se provádí na serveru mimo tuto webovou aplikaci.
## Funkce
- Přihlášení, změna hesla a správa uživatelských účtů.
- Oprávnění pro nahrávání, stahování a mazání souborů.
- Procházení adresářů, hledání v aktuální složce a filtrování souborů/složek.
- Nahrávání souborů i adresářů, drag & drop a zobrazení průběhu přenosu.
- Vytváření, přejmenování a mazání položek; stažení adresáře jako ZIP.
- Editor textových souborů, náhled obrázků, videa, audia a PDF.
- Sdílení médií odkazem s expirací a volitelným stažením.
- Přehled disků, Linux MD RAID polí, CPU, RAM a swapu.
- Společná postranní navigace, modré akcenty, světlý a tmavý režim bez závislosti na Bootstrapu.
## Technologie a požadavky
| Část | Požadavky |
| --- | --- |
| Webový server | Apache, například v XAMPP; pro lokální vývoj lze použít i vestavěný PHP server. |
| PHP | Projekt je lokálně ověřený na PHP 8.2.12. Používané závislosti jsou uvedené v `Web/composer.lock`. |
| Databáze | MySQL/MariaDB; původní SQL dump pochází z MySQL 8.0.41. |
| PHP moduly | `mysqli` s `mysqlnd` pro `get_result()`, `session`, `fileinfo`; `zip` pro ZIP operace. Testy vykreslení používají také `dom`. |
| Závislosti | Composer a `phpseclib/phpseclib` řady 3; v repozitáři je také adresář `Web/vendor`. |
| Úložiště | Dostupný SFTP server a účet s přístupem k požadované cestě. |
| Statistiky | SSH přístup k Linux serveru, příkazy `df`, `free`, `vmstat`, `awk`, `tail` a čitelný `/proc/mdstat` pro Linux MD RAID. |
| Prohlížeč | Moderní prohlížeč s podporou nativního `<dialog>`. PDF náhled načítá PDF.js z CDN. |
Rozbalování archivů kromě ZIP může vyžadovat další nástroje, například UnRAR, 7-Zip nebo tar podle zvoleného formátu a operačního systému. PHP musí mít zapisovatelný dočasný adresář pro práci s archivy.
## Rychlé zprovoznění
### 1. Stažení projektu a závislostí
```sh
git clone https://github.com/michalcz10/USB-RAID-Array.git
cd USB-RAID-Array/Web
composer install
```
Pokud už projekt máš stažený, spusť `composer install` v jeho adresáři `Web`. Pro instalaci podle existujícího lock souboru používej `install`.
### 2. Databáze a první správce
Postup je v [dokumentaci databáze](Documentation/Databaze.md). Obsahuje dvě varianty:
1. Import [původního SQL dumpu](Documentation/usbraidlogin.sql) a [doplnění tabulky pro sdílení](Documentation/share.sql).
2. Ruční vytvoření databáze a obou tabulek.
Součástí návodu je vytvoření prvního administrátora s bcrypt hashem hesla. Původní dump obsahuje také uživatelská data a sám nevytváří databázi ani tabulku `share`.
### 3. Konfigurace připojení
V souboru `Web/config/config.php` nastav vlastní připojení. Následující hodnoty jsou příklady; nahrazují odpovídající proměnné v existujícím souboru:
```php
$DB_HOST = 'localhost';
$DB_USER = 'usbraid_app';
$DB_PASS = 'DOPLNTE_HESLO_DB';
$DB_NAME = 'usbraidlogin';
$SFTP_HOST = 'ADRESA_SFTP_SERVERU';
$SFTP_USER = 'SFTP_UZIVATEL';
$SFTP_PASS = 'DOPLNTE_HESLO_SFTP';
$SSH_HOST = 'ADRESA_SSH_SERVERU';
$SSH_USER = 'SSH_UZIVATEL';
$SSH_PASS = 'DOPLNTE_HESLO_SSH';
```
SFTP a SSH mohou používat stejný server, ale mají samostatné konfigurační proměnné. Aktuální pomocné funkce používají výchozí port 22 a přihlášení heslem. Přihlašovací údaje do webu se ověřují proti tabulce `users`; jednotliví weboví uživatelé sdílejí nakonfigurované SFTP připojení.
Výchozí vzdálená cesta se načítá z `users.defPath` po přihlášení. Nastav například `/mnt/raid` podle skutečného připojení úložiště na vzdáleném serveru. Nejde o cestu na počítači s XAMPP. SFTP účet musí mít také odpovídající oprávnění v Linuxu.
### 4. Spuštění přes XAMPP
1. Spusť Apache a MySQL v XAMPP Control Panelu.
2. Zpřístupni adresář `Web` jako `C:\xampp\htdocs\Web` — zkopírováním nebo již nastaveným symlinkem.
3. Otevři `http://localhost/Web/` a přihlas se vytvořeným účtem.
4. Ve správě uživatelů nastav další účty, jejich vzdálené cesty a oprávnění.
Při úpravách projektu používej soubory, které Apache skutečně obsluhuje. U kopie v `htdocs` je potřeba změny překopírovat; symlink odkazuje přímo na pracovní adresář.
Alternativně lze z kořene repozitáře spustit lokální PHP server:
```sh
php -S 127.0.0.1:8080 -t Web
```
Pak otevři `http://127.0.0.1:8080/`. Databáze a vzdálený SFTP/SSH server musí být dostupné i v tomto režimu.
## Struktura projektu
```text
USB-RAID-Array/
├── README.md
├── LICENSE
├── Documentation/
│ ├── Databaze.md # import, ruční vytvoření tabulek a první správce
│ ├── usbraidlogin.sql # původní dump tabulky users včetně dat
│ ├── share.sql # doplnění schématu pro sdílené odkazy
│ └── ... # původní technická dokumentace a podklady
├── tests/
│ ├── workspace_test.php
│ ├── serverstats_test.php
│ └── remotepaths_test.php
└── Web/
├── index.php # přihlášení
├── config/config.php # databáze, SFTP a SSH
├── includes/workspace.php
├── content/
│ ├── adminpanel.php
│ ├── changepassword.php
│ └── ftp/ # správce souborů, náhledy a statistiky
├── css/ # společné styly a rozložení
├── js/ # téma a dialogy
├── composer.json
└── vendor/
```
## Kontroly
Z kořene repozitáře:
```sh
php tests/workspace_test.php
php tests/serverstats_test.php
php tests/remotepaths_test.php
php tests/backend_test.php
node --check Web/js/theme.js
node --check Web/js/ui.js
node --check Web/content/ftp/js/index.js
```
PHP testy používají testovací data a nepotřebují připojení k databázi ani SSH. Kontrolují vykreslení stránek, navigaci podle role, zpracování statistik a stavy RAID polí. Node.js slouží u uvedených příkazů ke kontrole syntaxe; pro běh aplikace není potřeba.
Test backendu navíc ověřuje přihlášení, oprávnění operací, hranice vzdálených cest, vytváření prázdných souborů a upload. Spouští handlery v oddělených PHP procesech s testovacími náhradami databáze a SFTP; vyžaduje dostupné `proc_open`.
V backendu používej popisné názvy lokálních proměnných ve stylu `camelCase`, například `$username`, `$passwordHash`, `$database`, `$statement`, `$directoryPath` a `$canUpload`. Konfigurační proměnné `$DB_*`, `$SFTP_*`, `$SSH_*` a existující klíče databáze, formulářů a session (`uname`, `pswd`, `defPath`, `upPer` apod.) zůstávají kvůli kompatibilitě zachované. Společné kontroly přihlášení, oprávnění a hesel jsou v `Web/includes/auth.php`; práci s POSIX cestami soustřeď do `Web/includes/remotepaths.php`.
## Časté problémy
| Problém | Co zkontrolovat |
| --- | --- |
| Chyba připojení k databázi | Běžící MySQL, název databáze, `$DB_*` a oprávnění databázového účtu. |
| Chybí tabulka `users` nebo `share` | Dokončení [nastavení databáze](Documentation/Databaze.md). |
| Přihlášení do webu nefunguje | `pswd` musí obsahovat bcrypt hash, nikoli samotné heslo. |
| `Login Failed` nebo `SSH login failed` | Nastavení SFTP/SSH, dostupnost serveru a vzdálená oprávnění. |
| Nefunguje upload větších souborů | `upload_max_filesize`, `post_max_size`, `max_file_uploads` a časové limity PHP/serveru. Po změně `php.ini` v XAMPP restartuj Apache. |
| Statistiky úložiště jsou stejné | `/` a uživatelská výchozí cesta mohou ležet na stejném souborovém systému. |
| RAID uvádí `Not configured` | Na připojeném serveru není v `/proc/mdstat` detekováno Linux MD pole; RAID na hostiteli virtuálního serveru se tímto způsobem nezobrazuje. |
| PDF se nenačte | Dostupnost PDF souboru, přístup přes SFTP a načtení PDF.js z CDN. |
| Zobrazuje se staré rozhraní | Obnov stránku včetně cache a ověř, že XAMPP načítá správnou kopii projektu. |
## Licence
Projekt je dostupný pod [licencí MIT](LICENSE). Autorem je Michal Sedlák.
-7
View File
@@ -1,7 +0,0 @@
{
"permissions": {
"allow": [
"Bash(git add *)"
]
}
}
+7 -19
View File
@@ -3,6 +3,7 @@
// Autoload dependencies (phpseclib)
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../includes/remotepaths.php';
use phpseclib3\Net\SFTP;
use phpseclib3\Net\SSH2;
@@ -18,7 +19,7 @@ $SFTP_HOST = '172.16.1.11';
$SFTP_USER = 'usbraid';
$SFTP_PASS = 'usbraid';
// Default remote path – uses session value if set
$defaultPath = $_SESSION['defPath'] ?? '/';
$defaultPath = normalizePath($_SESSION['defPath'] ?? '/');
// SSH credentials (placeholders)
$SSH_HOST = '172.16.1.11';
@@ -28,12 +29,12 @@ $SSH_PASS = 'usbraid';
/** Helper: Get a MySQLi connection */
function getDatabaseConnection(): mysqli {
global $DB_HOST, $DB_USER, $DB_PASS, $DB_NAME;
$conn = new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME);
if ($conn->connect_error) {
die('Connection failed: ' . $conn->connect_error);
$database = new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME);
if ($database->connect_error) {
die('Connection failed: ' . $database->connect_error);
}
$conn->set_charset('utf8');
return $conn;
$database->set_charset('utf8');
return $database;
}
/** Helper: Initialise SFTP connection */
@@ -56,17 +57,4 @@ function getSSHConnection(): SSH2 {
return $ssh;
}
/** Utility: Normalise a path */
function normalizePath(string $path): string {
$parts = array_filter(explode('/', $path), fn($part) => $part !== '' && $part !== '.');
$stack = [];
foreach ($parts as $part) {
if ($part === '..') {
array_pop($stack);
} else {
$stack[] = $part;
}
}
return '/' . implode('/', $stack);
}
?>
+105 -182
View File
@@ -1,150 +1,92 @@
<?php
session_start();
if (empty($_SESSION['admin']) || $_SESSION['admin'] !== true) {
session_destroy();
header("location: ../index.php");
exit();
}
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../includes/auth.php';
requireLogin('../index.php');
requirePermission('admin', 'You do not have permission to manage users.');
require_once __DIR__ . '/../config/config.php';
// $username = "UNAME";
// $password = "PSWD";
// $db = "usbraidlogin";
require_once __DIR__ . '/../includes/workspace.php';
$conn = getDatabaseConnection();
// charset already set in getDatabaseConnection()
if ($conn->connect_error) {
die("Database connection failed: " . $conn->connect_error);
}
$database = getDatabaseConnection();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['uname']) && isset($_POST['pswd'])) {
$uname = trim($_POST['uname']);
$pswd = trim($_POST['pswd']);
$is_admin = isset($_POST['admin']) ? 1 : 0;
$defPath = isset($_POST['defPath']) ? trim($_POST['defPath']) : '';
$delPer = isset($_POST['delPer']) ? (int)$_POST['delPer'] : 0;
$dowPer = isset($_POST['downPer']) ? (int)$_POST['downPer'] : 0;
$upPer = isset($_POST['upPer']) ? (int)$_POST['upPer'] : 0;
$username = is_string($_POST['uname'] ?? null) ? trim($_POST['uname']) : '';
$password = is_string($_POST['pswd'] ?? null) ? $_POST['pswd'] : '';
$defaultPath = is_string($_POST['defPath'] ?? null) ? normalizePath(trim($_POST['defPath'])) : '/';
$isAdmin = isset($_POST['admin']) ? 1 : 0;
$canDelete = isset($_POST['delPer']) ? 1 : 0;
$canDownload = isset($_POST['downPer']) ? 1 : 0;
$canUpload = isset($_POST['upPer']) ? 1 : 0;
$_SESSION['message_type'] = 'error';
if (empty($uname) || empty($pswd)) {
$_SESSION['message'] = 'Error: Username and password are required!';
$_SESSION['message_type'] = 'error';
} else if (!CheckPassword($pswd)) {
$_SESSION['message'] = 'Error: Password must be at least 8 characters long, contain at least one number and one uppercase letter!';
$_SESSION['message_type'] = 'error';
} else {
$sql_check = "SELECT * FROM users WHERE uname = ?";
$stmt_check = $conn->prepare($sql_check);
if (!$stmt_check) {
$_SESSION['message'] = 'Error: Database preparation failed.';
$_SESSION['message_type'] = 'error';
if ($username === '' || $password === '') {
$_SESSION['message'] = 'Error: Username and password are required!';
} elseif (!isPasswordValid($password)) {
$_SESSION['message'] = 'Error: Password must be at least 8 characters long, contain at least one number and one uppercase letter!';
} else {
try {
$statement = $database->prepare('SELECT uname FROM users WHERE uname = ?');
$statement->bind_param('s', $username);
$statement->execute();
$result = $statement->get_result();
$userExists = $result->num_rows > 0;
$result->free();
$statement->close();
if ($userExists) {
$_SESSION['message'] = 'Error: Username already exists!';
} else {
$stmt_check->bind_param("s", $uname);
$stmt_check->execute();
$result_check = $stmt_check->get_result();
if ($result_check->num_rows > 0) {
$_SESSION['message'] = 'Error: Username already exists!';
$_SESSION['message_type'] = 'error';
} else {
$sql_insert = "INSERT INTO users (uname, pswd, admin, defPath, delPer, downPer, upPer) VALUES (?, ?, ?, ?, ?, ?, ?)";
$stmt_insert = $conn->prepare($sql_insert);
if (!$stmt_insert) {
$_SESSION['message'] = 'Error: Database preparation failed.';
$_SESSION['message_type'] = 'error';
} else {
$hash = password_hash($pswd, PASSWORD_BCRYPT);
if (!$hash) {
$_SESSION['message'] = 'Error: Password hashing failed.';
$_SESSION['message_type'] = 'error';
} else {
$stmt_insert->bind_param("ssisiii", $uname, $hash, $is_admin, $defPath, $delPer, $dowPer, $upPer);
if ($stmt_insert->execute()) {
$_SESSION['message'] = 'User added successfully!';
$_SESSION['message_type'] = 'success';
} else {
$_SESSION['message'] = 'Error: Failed to add user. Please try again later.';
$_SESSION['message_type'] = 'error';
}
}
}
}
$stmt_check->close();
if (isset($stmt_insert)) {
$stmt_insert->close();
}
$passwordHash = password_hash($password, PASSWORD_BCRYPT);
$statement = $database->prepare('INSERT INTO users (uname, pswd, admin, defPath, delPer, downPer, upPer) VALUES (?, ?, ?, ?, ?, ?, ?)');
$statement->bind_param('ssisiii', $username, $passwordHash, $isAdmin, $defaultPath, $canDelete, $canDownload, $canUpload);
$statement->execute();
$statement->close();
$_SESSION['message'] = 'User added successfully!';
$_SESSION['message_type'] = 'success';
}
} catch (mysqli_sql_exception $exception) {
error_log($exception->getMessage());
$_SESSION['message'] = 'Error: Failed to add user. Please try again later.';
}
header("Location: adminpanel.php");
exit();
} else {
$_SESSION['message'] = 'Error: Missing form data!';
$_SESSION['message_type'] = 'error';
header("Location: adminpanel.php");
exit();
}
$database->close();
header('Location: adminpanel.php');
exit;
}
if (isset($_GET['delete'])) {
$delete_uname = htmlspecialchars($_GET['delete']);
$sql = "DELETE FROM users WHERE uname=?";
$stmt = $conn->prepare($sql);
if ($stmt) {
$stmt->bind_param("s", $delete_uname);
if ($stmt->execute()) {
$_SESSION['message'] = 'User deleted successfully!';
$_SESSION['message_type'] = 'success';
} else {
$_SESSION['message'] = 'Error: Failed to delete user.';
$_SESSION['message_type'] = 'error';
}
$stmt->close();
} else {
$_SESSION['message'] = 'Error: Database preparation failed.';
if (isset($_GET['delete']) && is_string($_GET['delete'])) {
$usernameToDelete = $_GET['delete'];
try {
$statement = $database->prepare('DELETE FROM users WHERE uname = ?');
$statement->bind_param('s', $usernameToDelete);
$statement->execute();
$statement->close();
$_SESSION['message'] = 'User deleted successfully!';
$_SESSION['message_type'] = 'success';
} catch (mysqli_sql_exception $exception) {
error_log($exception->getMessage());
$_SESSION['message'] = 'Error: Failed to delete user.';
$_SESSION['message_type'] = 'error';
}
header("Location: adminpanel.php");
exit();
$database->close();
header('Location: adminpanel.php');
exit;
}
$result = $conn->query("SELECT uname, admin, defPath, delPer, downPer, upPer FROM users");
$result = $database->query('SELECT uname, admin, defPath, delPer, downPer, upPer FROM users');
$message = $_SESSION['message'] ?? '';
$message_type = $_SESSION['message_type'] ?? '';
unset($_SESSION['message']);
unset($_SESSION['message_type']);
function CheckPassword($password) {
if (strlen($password) < 8) {
return false;
}
if (!preg_match('/[0-9]/', $password)) {
return false;
}
if (!preg_match('/[A-Z]/', $password)) {
return false;
}
return true;
}
$messageType = $_SESSION['message_type'] ?? '';
unset($_SESSION['message'], $_SESSION['message_type']);
?>
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin Panel</title>
<link rel="icon" href="../img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../css/bootstrap.css">
<link rel="stylesheet" href="../css/modern.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="css/adminpanel.css">
<script src="../js/bootstrap.bundle.js"></script>
<link rel="stylesheet" href="<?= workspaceAsset('../', 'css/modern.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../', 'css/workspace.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../', 'content/css/adminpanel.css') ?>">
<script>
function confirmDelete(uname) {
if (confirm('Are you sure you want to delete this user?')) {
@@ -152,45 +94,39 @@ function CheckPassword($password) {
}
}
</script>
<script src="<?= workspaceAsset('../', 'js/theme.js') ?>"></script>
<script src="<?= workspaceAsset('../', 'js/ui.js') ?>" defer></script>
</head>
<body class="text-center">
<div class="d-flex justify-content-end p-3">
<button id="themeToggle" class="btn btn-sm theme-toggle">
<i class="bi"></i>
<span id="themeText"></span>
</button>
</div>
<?php renderWorkspaceStart([
'root' => '../',
'active' => 'users',
'section' => 'User management',
'eyebrow' => 'Workspace administration',
'title' => 'User management',
'description' => 'Manage accounts, storage locations and access permissions.',
'class' => 'users-page',
]); ?>
<section class="users-layout">
<div class="custom-container dark-box">
<header class="row border-bottom m-5">
<h1>USB RAID Array</h1>
<div class="mb-3 p-3">
<a href="logout.php" class="btn btn-danger">Logout</a>
<a href="changepassword.php" class="btn btn-warning">Change Password</a>
<a href="ftp/index.php" class="btn btn-primary">SFTP</a>
<a href="ftp/serverstat.php" class="btn btn-primary">Server Status</a>
</div>
</header>
<section class="row">
<article class="col border border-2 border-primary rounded p-2 surface-panel">
<h2 class="mb-4">User Management</h2>
<?php if ($message): ?>
<div class="alert alert-<?php echo $message_type === 'success' ? 'success' : 'danger'; ?>">
<div class="alert alert-<?php echo $messageType === 'success' ? 'success' : 'danger'; ?>">
<?php echo htmlspecialchars($message); ?>
</div>
<?php endif; ?>
<form method="POST" action="" class="mb-4 add-user-form">
<div class="content-panel user-create"><div class="panel-heading"><h2>Add a member</h2><p>Set up an account and choose its access.</p></div>
<form method="POST" action="" class="add-user-form">
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" name="uname" class="form-control" placeholder="Username" required>
<label class="form-label" for="memberUsername">Username</label>
<input type="text" name="uname" id="memberUsername" class="form-control" placeholder="Username" required>
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" name="pswd" class="form-control" placeholder="Password" required>
<label class="form-label" for="memberPassword">Password</label>
<input type="password" name="pswd" id="memberPassword" class="form-control" placeholder="Password" required>
</div>
<div class="mb-3">
<label class="form-label">Default Path</label>
<input type="text" name="defPath" class="form-control" placeholder="Default Path (e.g., /mnt/raid)" required>
<label class="form-label" for="memberPath">Default path</label>
<input type="text" name="defPath" id="memberPath" class="form-control" placeholder="Default Path (e.g., /mnt/raid)" required>
</div>
<div class="d-flex flex-column align-items-center mb-3">
<div class="form-check mb-2">
@@ -211,9 +147,11 @@ function CheckPassword($password) {
</div>
</div>
<button type="submit" class="btn btn-primary">Add User</button>
</form>
<h3 class="mb-3">Users List</h3>
<table class="table table-bordered">
</form></div>
<div class="content-panel user-list"><div class="panel-heading"><h2>Workspace members</h2><p>Accounts and permissions for your storage.</p></div>
<div class="table-scroll"><table class="table users-table">
<thead class="table-dark">
<tr>
<th>Username</th>
@@ -226,41 +164,26 @@ function CheckPassword($password) {
</tr>
</thead>
<tbody>
<?php while ($row = $result->fetch_assoc()) { ?>
<?php while ($user = $result->fetch_assoc()) { ?>
<tr>
<td><?php echo htmlspecialchars($row['uname']); ?></td>
<td><?php echo $row['admin'] ? 'Yes' : 'No'; ?></td>
<td><?php echo htmlspecialchars($row['defPath']); ?></td>
<td><?php echo $row['delPer'] ? 'Yes' : 'No'; ?></td>
<td><?php echo $row['downPer'] ? 'Yes' : 'No'; ?></td>
<td><?php echo $row['upPer'] ? 'Yes' : 'No'; ?></td>
<td><?php echo htmlspecialchars($user['uname']); ?></td>
<td><?php echo $user['admin'] ? 'Yes' : 'No'; ?></td>
<td><?php echo htmlspecialchars($user['defPath']); ?></td>
<td><?php echo $user['delPer'] ? 'Yes' : 'No'; ?></td>
<td><?php echo $user['downPer'] ? 'Yes' : 'No'; ?></td>
<td><?php echo $user['upPer'] ? 'Yes' : 'No'; ?></td>
<td>
<button class="btn btn-danger btn-sm" onclick="confirmDelete('<?php echo htmlspecialchars($row['uname']); ?>')">Delete</button>
<button class="btn btn-danger btn-sm" onclick="confirmDelete(<?= workspaceEscape(json_encode($user['uname'])) ?>)">Delete</button>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</article>
</section>
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3 m-3">
<span class="text-muted">Developed by Michal Sedlák</span>
<div class="d-flex gap-3">
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
<img src="../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
</a>
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
<img src="../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
</a>
</table></div>
</div>
</footer>
<script src="../js/theme.js"></script>
</div>
</section>
<?php renderWorkspaceEnd(); ?>
</body>
</html>
<?php
$conn->close();
$database->close();
?>
+71 -133
View File
@@ -1,134 +1,86 @@
<?php
session_start();
if (!isset($_SESSION['uname'])) {
header("location: ../index.php");
exit();
}
require_once __DIR__ . '/../includes/auth.php';
requireLogin('../index.php');
require_once __DIR__ . '/../config/config.php';
// $username = "UNAME";
// $password = "PSWD";
// $db = "usbraidlogin";
require_once __DIR__ . '/../includes/workspace.php';
$message = "";
$messageType = "";
$message = '';
$messageType = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['current_password'], $_POST['new_password'], $_POST['confirm_password'])) {
$current_password = htmlspecialchars($_POST['current_password']);
$new_password = htmlspecialchars($_POST['new_password']);
$confirm_password = htmlspecialchars($_POST['confirm_password']);
$uname = $_SESSION['uname'];
$messageType = 'danger';
$currentPassword = $_POST['current_password'] ?? null;
$newPassword = $_POST['new_password'] ?? null;
$confirmPassword = $_POST['confirm_password'] ?? null;
if(!CheckPassword($new_password)) {
$message = "New password must be at least 8 characters long, contain at least one number and one uppercase letter!";
$messageType = "danger";
} else if ($new_password !== $confirm_password) {
$message = "New passwords do not match!";
$messageType = "danger";
} else {
$conn = getDatabaseConnection();
// charset already set in getDatabaseConnection()
if ($conn->connect_error) {
$message = "Database connection failed!";
$messageType = "danger";
} else {
$sql = "SELECT pswd FROM users WHERE uname=?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $uname);
$stmt->execute();
$result = $stmt->get_result();
if ($result && $result->num_rows > 0) {
$row = $result->fetch_assoc();
$stored_hash = $row["pswd"];
if (password_verify($current_password, $stored_hash)) {
$new_hash = password_hash($new_password, PASSWORD_BCRYPT);
$update_sql = "UPDATE users SET pswd=? WHERE uname=?";
$update_stmt = $conn->prepare($update_sql);
$update_stmt->bind_param("ss", $new_hash, $uname);
if ($update_stmt->execute()) {
$message = "Password changed successfully!";
$messageType = "success";
} else {
$message = "Error updating password: " . $conn->error;
$messageType = "danger";
}
$update_stmt->close();
} else {
$message = "Current password is incorrect!";
$messageType = "danger";
}
} else {
$message = "User not found!";
$messageType = "danger";
}
$stmt->close();
$conn->close();
}
}
if (!is_string($currentPassword) || !is_string($newPassword) || !is_string($confirmPassword)) {
$message = 'All fields are required!';
} elseif (!isPasswordValid($newPassword)) {
$message = 'New password must be at least 8 characters long, contain at least one number and one uppercase letter!';
} elseif ($newPassword !== $confirmPassword) {
$message = 'New passwords do not match!';
} else {
$message = "All fields are required!";
$messageType = "danger";
}
}
$database = getDatabaseConnection();
try {
$username = $_SESSION['uname'];
$statement = $database->prepare('SELECT pswd FROM users WHERE uname = ?');
$statement->bind_param('s', $username);
$statement->execute();
$result = $statement->get_result();
$user = $result->fetch_assoc();
$result->free();
$statement->close();
function CheckPassword($password) {
if (strlen($password) < 8) {
return false;
if (!$user) {
$message = 'User not found!';
} elseif (!password_verify($currentPassword, $user['pswd'])) {
$message = 'Current password is incorrect!';
} else {
$passwordHash = password_hash($newPassword, PASSWORD_BCRYPT);
$statement = $database->prepare('UPDATE users SET pswd = ? WHERE uname = ?');
$statement->bind_param('ss', $passwordHash, $username);
$statement->execute();
$statement->close();
$message = 'Password changed successfully!';
$messageType = 'success';
}
} catch (mysqli_sql_exception $exception) {
error_log($exception->getMessage());
$message = 'Unable to change password. Please try again.';
} finally {
$database->close();
}
}
if (!preg_match('/[0-9]/', $password)) {
return false;
}
if (!preg_match('/[A-Z]/', $password)) {
return false;
}
return true;
}
?>
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Change Password</title>
<link rel="icon" href="../img/favicon.ico" type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../css/bootstrap.css">
<link rel="stylesheet" href="../css/modern.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="css/changepassword.css">
<script src="../js/bootstrap.bundle.js"></script>
<link rel="stylesheet" href="<?= workspaceAsset('../', 'css/modern.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../', 'css/workspace.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../', 'content/css/changepassword.css') ?>">
<script src="<?= workspaceAsset('../', 'js/theme.js') ?>"></script>
<script src="<?= workspaceAsset('../', 'js/ui.js') ?>" defer></script>
</head>
<body class="text-center">
<div class="d-flex justify-content-end p-3">
<button id="themeToggle" class="btn btn-sm theme-toggle">
<i class="bi"></i>
<span id="themeText"></span>
</button>
</div>
<?php renderWorkspaceStart([
'root' => '../',
'active' => 'account',
'section' => 'Account settings',
'eyebrow' => 'Your account',
'title' => 'Account settings',
'description' => 'Keep your workspace account secure and up to date.',
'class' => 'account-page',
]); ?>
<div class="custom-container dark-box">
<header class="row border-bottom my-3 my-md-5">
<h1>USB RAID Array</h1>
<div class="mb-3 p-3">
<div class="btn-group-responsive">
<a href="logout.php" class="btn btn-danger">Logout</a>
<a href="ftp/index.php" class="btn btn-primary">Back to Files</a>
<?php if (isset($_SESSION["admin"]) && $_SESSION["admin"] == true) { ?>
<a href="adminpanel.php" class="btn btn-primary">Admin Panel</a>
<?php } ?>
</div>
</div>
</header>
<section class="row content-section">
<article class="col-12 border border-2 border-primary rounded p-2 p-md-4 surface-panel">
<h2 class="mb-4">Change Password</h2>
<section class="account-layout">
<article class="content-panel">
<div class="panel-heading"><h2>Change password</h2><p>Choose a strong password to protect your files.</p></div>
<?php if ($message): ?>
<div class="alert alert-<?php echo $messageType; ?> mb-4">
@@ -138,37 +90,23 @@ function CheckPassword($password) {
<form method="POST" action="" class="mb-4 change-password-form">
<div class="mb-3">
<label class="form-label">Current Password</label>
<input type="password" name="current_password" class="form-control" required>
<label class="form-label" for="currentPassword">Current password</label>
<input type="password" id="currentPassword" autocomplete="current-password" name="current_password" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">New Password</label>
<input type="password" name="new_password" class="form-control" required>
<label class="form-label" for="newPassword">New password</label>
<input type="password" id="newPassword" autocomplete="new-password" name="new_password" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Confirm New Password</label>
<input type="password" name="confirm_password" class="form-control" required>
<label class="form-label" for="confirmPassword">Confirm new password</label>
<input type="password" id="confirmPassword" autocomplete="new-password" name="confirm_password" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">Change Password</button>
</form>
</article>
<aside class="account-help"><span class="eyebrow">Password requirements</span><h3>A little extra protection</h3><p>Use at least 8 characters, one uppercase letter and one number.</p><p>You can return to your files at any time using the workspace navigation.</p></aside>
</section>
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3 m-3">
<span class="text-muted">Developed by Michal Sedlák</span>
<div class="d-flex gap-3 flex-wrap justify-content-center">
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
<img src="../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
</a>
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
<img src="../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
</a>
</div>
</footer>
<script src="../js/theme.js"></script>
</div>
<?php renderWorkspaceEnd(); ?>
</body>
</html>
+13 -66
View File
@@ -1,66 +1,13 @@
html, body {
height: 100%;
margin: 0;
}
.custom-container {
display: flex;
flex-direction: column;
min-height: 100vh;
max-width: 60%;
margin: 0 auto;
}
.custom-container.dark-box {
min-height: calc(100vh - 4rem);
width: min(60%, 1200px);
}
section.row {
flex: 1;
}
@media (max-width: 1120px) {
.custom-container {
max-width: 90%;
}
.custom-container.dark-box {
width: 90%;
}
}
@media (max-width: 700px) {
.custom-container.dark-box {
width: calc(100% - 2rem);
min-height: calc(100vh - 2rem);
margin: 1rem auto;
}
body {
min-width: 0;
}
}
.add-user-form {
max-width: 400px;
margin: 0 auto;
}
.form-check {
display: flex;
align-items: center;
width: 100%;
justify-content: flex-start;
}
.form-check-input {
margin-right: 10px;
}
.form-check-label {
margin-left: 0.5rem;
}
.hover-effect {
transition: opacity 0.3s ease;
}
.hover-effect:hover {
opacity: 0.8;
}
.theme-light .dark-logo {
display: none;
}
.theme-dark .light-logo {
display: none;
}
.users-layout { display:grid; grid-template-columns:300px minmax(0,1fr); align-items:start; gap:22px; }
.users-layout > .alert { grid-column:1/-1; margin:0; }
.add-user-form { margin:0; text-align:left; }
.add-user-form .form-check { width:100%; margin-bottom:12px; font-size:12px; }
.add-user-form .btn { width:100%; }
.users-table { min-width:650px; }
.users-table th { font-size:10px; padding:12px 10px; white-space:nowrap; }
.users-table td { font-size:12px; padding:14px 10px; }
.users-table td:first-child { font-weight:600; }
.users-table td:nth-child(3) { font-family:ui-monospace,monospace; white-space:nowrap; }
.user-create .form-label { font-size:12px; }
@media(max-width:1200px) { .users-layout { grid-template-columns:1fr; } .add-user-form { display:grid; grid-template-columns:1fr 1fr; gap:0 24px; } .add-user-form > .d-flex { grid-column:1/-1; display:grid; grid-template-columns:1fr 1fr; gap:8px; } .add-user-form > .btn { width:auto; justify-self:start; } }
@media(max-width:600px) { .add-user-form { display:block; } .add-user-form > .d-flex { display:flex; align-items:flex-start; gap:0; } }
+6 -56
View File
@@ -1,56 +1,6 @@
.container-fluid {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
.content-section {
max-width: 800px;
margin: 0 auto;
}
.change-password-form {
max-width: 100%;
margin: 0 auto;
}
@media (min-width: 768px) {
.change-password-form {
max-width: 400px;
}
}
.hover-effect {
transition: opacity 0.3s ease;
}
.hover-effect:hover {
opacity: 0.8;
}
.theme-light .dark-logo {
display: none;
}
.theme-dark .light-logo {
display: none;
}
.btn-group-responsive {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
}
h1 {
font-size: 1.8rem;
}
@media (min-width: 576px) {
h1 {
font-size: 2.5rem;
}
}
.account-layout { display:grid; grid-template-columns:minmax(0,560px) minmax(200px,1fr); align-items:start; gap:40px; }
.change-password-form { margin:0; }
.change-password-form .form-label { font-size:12px; }
.account-help { max-width:320px; padding:24px 0; color:var(--muted); font-size:13px; }
.account-help h3 { color:var(--text); margin-top:12px; font-size:18px; }
@media(max-width:1000px) { .account-layout { grid-template-columns:1fr; gap:16px; } .account-help { max-width:100%; padding:12px 4px; } }
+11 -11
View File
@@ -1,15 +1,14 @@
<?php
// Include this at the top to see potential errors
// Comment out in production
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../includes/auth.php';
requireLogin('../../index.php');
requirePermission('upPer', 'You do not have permission to perform this operation.');
require_once __DIR__ . '/../../config/config.php';
$sftp = initializeSFTP();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'createDir') {
$currentPath = isset($_POST['currentPath']) ? normalizePath($_POST['currentPath']) : $defaultPath;
$currentPath = requireRemotePath($_POST['currentPath'] ?? $defaultPath, $defaultPath);
if (!isset($_POST['dirName'])) {
http_response_code(400);
@@ -17,17 +16,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
exit;
}
$dirName = $_POST['dirName'];
$directoryName = $_POST['dirName'];
$dirName = preg_replace('/[^\w\-\.]/', '_', $dirName);
$directoryName = preg_replace('/[^\w\-\.]/', '_', $directoryName);
if (strpos($currentPath, $defaultPath) !== 0) {
$currentPath = $defaultPath;
if (!isRemoteNameValid($directoryName)) {
http_response_code(400);
exit('Invalid directory name.');
}
$newDirPath = $currentPath . '/' . $dirName;
$newDirectoryPath = rtrim($currentPath, '/') . '/' . $directoryName;
if ($sftp->mkdir($newDirPath)) {
if ($sftp->mkdir($newDirectoryPath)) {
echo "Directory created successfully!";
} else {
http_response_code(500);
+10 -31
View File
@@ -1,29 +1,14 @@
<?php
// Include this at the top to see potential errors
// Comment out in production
ini_set('display_errors', 1);
error_reporting(E_ALL);
session_start();
if(!isset($_SESSION['uname'])){
header("location: ../index.html");
session_destroy();
exit;
}
if(!isset($_SESSION["upPer"]) || $_SESSION["upPer"] != true) {
http_response_code(403);
echo "You don't have permission to create files.";
exit;
}
require_once __DIR__ . '/../../includes/auth.php';
requireLogin('../../index.php');
requirePermission('upPer', 'You do not have permission to perform this operation.');
require_once __DIR__ . '/../../config/config.php';
$sftp = initializeSFTP();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'createFile') {
$currentPath = isset($_POST['currentPath']) ? normalizePath($_POST['currentPath']) : $defaultPath;
$currentPath = requireRemotePath($_POST['currentPath'] ?? $defaultPath, $defaultPath);
if (!isset($_POST['fileName'])) {
http_response_code(400);
@@ -41,22 +26,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
$fileName = preg_replace('/[^\w\-\.]/', '_', $fileName);
if (strpos($currentPath, $defaultPath) !== 0) {
$currentPath = $defaultPath;
if (!isRemoteNameValid($fileName)) {
http_response_code(400);
exit('Invalid file name.');
}
$newFilePath = $currentPath . '/' . $fileName;
$tempFile = tempnam(sys_get_temp_dir(), 'new_file_');
file_put_contents($tempFile, '');
if ($sftp->put($newFilePath, $tempFile)) {
@unlink($tempFile);
$newFilePath = rtrim($currentPath, '/') . '/' . $fileName;
header('Content-Type: application/json; charset=UTF-8');
if ($sftp->put($newFilePath, '')) {
echo json_encode(['success' => true, 'filePath' => $newFilePath]);
} else {
@unlink($tempFile);
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to create file.']);
}
+15 -141
View File
@@ -1,142 +1,16 @@
html, body {
height: 100%;
margin: 0;
overflow-x: hidden;
overflow-y: auto;
}
.pathbar { display:flex; align-items:center; flex-wrap:wrap; gap:12px; padding:13px 16px; background:var(--surface); border:1px solid var(--line); border-radius:10px; margin-bottom:20px; font-size:12px; overflow-wrap:anywhere; }
.pathbar a { color:var(--text); } .pathbar a:first-child { color:var(--blue); } .path-separator { color:var(--muted); } .path-label { margin-left:auto; color:var(--muted); font:11px ui-monospace,monospace; }
.file-panel { border:1px solid var(--line); border-radius:12px; background:var(--surface); box-shadow:var(--shadow); overflow:hidden; }
.browser-toolbar { padding:20px; display:flex; align-items:center; gap:12px; border-bottom:1px solid var(--line); }
.search-field { display:flex; align-items:center; gap:10px; margin:0; padding:0 12px; border:1px solid var(--line); border-radius:8px; width:360px; max-width:100%; background:var(--bg); color:var(--muted); font-weight:400; }
.search-field > span { font-size:24px; } .search-field input { width:100%; min-width:0; padding:10px 0; border:0; background:transparent; color:var(--text); outline:none; } .search-field:focus-within { outline:2px solid var(--blue); } .search-field input::placeholder { color:var(--muted); } kbd { font:11px ui-monospace,monospace; padding:0 5px; border:1px solid var(--line); border-radius:3px; }
.browser-toolbar select { width:130px; margin-left:auto; font-size:12px; padding:10px; }
.action-buttons { display:flex; flex-wrap:wrap; gap:8px; padding:18px 20px 0; } .action-buttons .btn { font-size:12px; } .action-buttons .btn-primary { order:-1; }
.dropzone { margin:18px 20px 22px; border:1px dashed var(--line); background:var(--bg); border-radius:9px; display:flex; align-items:center; justify-content:center; gap:14px; padding:20px; text-align:left; }
.dropzone.dragover { border-color:var(--blue); background:var(--blue-soft); } .upload-symbol { display:grid; place-items:center; width:36px; height:36px; border:1px solid var(--line); border-radius:9px; background:var(--surface); font-size:22px; color:var(--blue); } .dropzone strong { font-weight:550; font-size:12px; } .dropzone small { display:block; color:var(--muted); font-size:11px; margin-top:3px; }
.file-table-scroll { overflow-x:auto; } .file-table { min-width:650px; } .file-table th { padding:12px 20px; } .file-table td { padding:14px 20px; font-size:12px; } .file-table th:first-child { width:48%; } .file-table th:nth-child(2) { width:13%; } .file-table th:last-child { width:39%; } .file-table td:first-child { font-weight:550; } .file-table td:first-child a { color:var(--text); } .file-table .btn { font-size:11px; padding:5px 8px; min-height:30px; background:transparent; border-color:transparent; } .file-table .btn:hover { background:var(--surface); border-color:var(--line); } .file-table td:last-child { white-space:nowrap; } .file-table td:nth-child(2) { color:var(--muted); font-family:ui-monospace,monospace; font-size:11px; }
.file-icon { display:inline-grid; place-items:center; width:30px; height:32px; margin-right:10px; color:var(--muted); background:var(--surface-alt); border-radius:7px; vertical-align:middle; font-size:18px; } .folder-icon { color:var(--blue); background:var(--blue-soft); } .file-table .badge { margin-left:5px; }
.browser-status { display:flex; justify-content:space-between; padding:13px 20px; color:var(--muted); font-size:11px; } .empty-state { text-align:center; padding:48px 20px; color:var(--muted); } .empty-state > span { font-size:38px; } .empty-state h3 { color:var(--text); margin:6px 0; } .empty-state p { margin:0; }
.file-content > .alert { margin:16px 20px; }
.custom-container {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
padding: 0;
max-width: 100%;
overflow: visible;
}
section.row {
flex: 1;
margin: 0;
width: 100%;
justify-content: center;
}
article.col-8 {
max-width: 1600px;
}
@media (max-width: 1800px) {
article.col-8 {
flex: 0 0 auto;
width: 95%;
}
}
@media (max-width: 768px) {
.custom-container {
height: auto;
min-height: initial;
}
section.row {
flex: 0 0 auto;
}
footer {
margin-top: 20px;
position: relative;
}
body {
height: auto;
min-height: initial;
}
article {
padding-bottom: 20px;
}
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
table-layout: fixed;
}
th, td {
padding: 10px;
border: 1px solid #ddd;
text-align: left;
word-wrap: break-word;
overflow-wrap: break-word;
}
.dropzone {
width: 100%;
padding: 20px;
border: 2px dashed #007bff;
border-radius: 10px;
text-align: center;
margin-bottom: 20px;
cursor: pointer;
box-sizing: border-box;
}
.dropzone.dragover {
background-color: #e0f7fa;
}
.action-buttons {
margin-bottom: 15px;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.action-buttons button {
margin-right: 0;
}
body {
min-width: 500px;
}
.hover-effect {
transition: opacity 0.3s ease;
}
.hover-effect:hover {
opacity: 0.8;
}
.theme-light .dark-logo {
display: none;
}
.theme-dark .light-logo {
display: none;
}
footer {
margin-top: auto;
padding: 20px;
border-top: 1px solid #ddd;
width: 100%;
}
/* Give the SFTP file browser its own surface, distinct from the outer page. */
article.col-8 {
background: linear-gradient(145deg, var(--sftp-bg-start), var(--sftp-bg-end));
border-color: rgba(77, 145, 255, 0.3) !important;
border-radius: 1rem !important;
box-shadow: 0 12px 32px rgba(3, 8, 18, 0.2);
}
html[data-bs-theme="light"] article.col-8 {
background: linear-gradient(145deg, #ffffff, #edf3fb);
border-color: rgba(59, 130, 246, 0.4) !important;
box-shadow: 0 18px 48px rgba(31, 58, 92, 0.1);
}
@media(max-width:760px) { .path-label { display:none; } .browser-toolbar { padding:14px; gap:8px; } .search-field { width:auto; flex:1; min-width:0; } .browser-toolbar select { width:100px; } .action-buttons { padding:14px 14px 0; } .dropzone { margin:14px; } }
+6 -54
View File
@@ -1,54 +1,6 @@
.editor-container {
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 20px;
position: relative;
}
#editor {
width: 100%;
min-height: 400px;
font-family: monospace;
padding: 10px;
white-space: pre;
overflow: auto;
resize: vertical;
tab-size: 4;
-moz-tab-size: 4;
padding-left: 55px; /* Make room for line numbers */
}
.line-numbers {
position: absolute;
left: 0;
top: 0;
width: 45px;
text-align: right;
padding: 10px 5px 10px 0;
border-right: 1px solid #ccc;
color: #999;
user-select: none;
font-family: monospace;
overflow: hidden;
z-index: 1;
background-color: #f8f9fa; /* Light mode default */
}
[data-bs-theme="dark"] .line-numbers {
background-color: #212529; /* Dark mode background */
border-right-color: #495057; /* Darker border for dark mode */
color: #6c757d; /* Lighter text for dark mode */
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.readonly-notice {
color: #dc3545;
font-weight: bold;
margin-left: 10px;
}
.editor-container { position:relative; border:1px solid var(--line); border-radius:10px; overflow:hidden; margin-bottom:20px; }
#editor { width:100%; min-height:55vh; padding:16px; font:13px/1.65 ui-monospace,Consolas,monospace; white-space:pre; overflow:auto; resize:vertical; tab-size:4; border:0; border-radius:0; background:var(--bg); }
.editor-container:has(.line-numbers) #editor { padding-left:64px; }
.line-numbers { position:absolute; left:0; top:0; width:48px; text-align:right; padding:16px 10px 16px 0; border-right:1px solid var(--line); color:var(--muted); user-select:none; font:13px/1.65 ui-monospace,Consolas,monospace; overflow:hidden; z-index:1; background:var(--surface-alt); }
.readonly-notice { color:var(--muted); font-size:12px; }
.editor-page .d-flex { flex-wrap:wrap; gap:8px; }
+15 -253
View File
@@ -1,253 +1,15 @@
body {
min-height: 100vh;
margin: 0;
padding: 0;
}
#pdfContainer {
width: 100%;
min-height: calc(100vh - 80px);
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
}
#loadingMessage {
position: absolute;
top: 0;
left: 0;
z-index: 1000;
background-color: var(--bs-body-bg);
}
[data-bs-theme="dark"] #loadingMessage {
background-color: rgba(33, 37, 41, 0.9);
}
[data-bs-theme="light"] #loadingMessage {
background-color: rgba(248, 249, 250, 0.9);
}
.page-container {
background-color: white;
margin: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
border-radius: 4px;
overflow: hidden;
}
.controls {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
}
canvas {
display: block;
}
footer {
margin-top: auto;
padding: 20px;
border-top: 1px solid #ddd;
width: 100%;
}
.hover-effect {
transition: opacity 0.3s ease;
}
.hover-effect:hover {
opacity: 0.8;
}
.theme-light .dark-logo {
display: none;
}
.theme-dark .light-logo {
display: none;
}
.file-info table {
table-layout: fixed;
word-wrap: break-word;
}
.actionButton {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
}
.fullscreen-mode {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-color: var(--bs-body-bg);
z-index: 9999;
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.fullscreen-mode header,
.fullscreen-mode footer,
.fullscreen-mode .theme-toggle,
.fullscreen-mode .actionButton {
display: none !important;
}
.fullscreen-mode #pdfContainer {
flex: 1;
margin: 0;
padding: 0;
height: 100vh;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.fullscreen-mode .controls {
position: fixed;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
margin: 0;
padding: 6px 12px;
background-color: rgba(var(--bs-body-bg-rgb), 0.3);
backdrop-filter: blur(5px);
transition: all 0.3s ease;
opacity: 0.3;
z-index: 1000;
border-radius: .375rem;
width: auto;
max-width: 95%;
}
.fullscreen-mode .controls:hover {
opacity: 1;
background-color: rgba(var(--bs-body-bg-rgb), 0.9);
box-shadow: 0 0 15px rgba(0,0,0,0.2);
}
.fullscreen-mode[data-bs-theme="light"] .btn-light {
background-color: rgba(233, 236, 239, 0.7);
border-color: rgba(222, 226, 230, 0.7);
}
.fullscreen-mode[data-bs-theme="dark"] .btn-light {
background-color: rgba(52, 58, 64, 0.7);
border-color: rgba(73, 80, 87, 0.7);
}
.fullscreen-mode .btn-light:hover {
transform: scale(1.05);
transition: transform 0.2s ease;
}
.fullscreen-mode .btn-light.disabled {
background-color: rgba(var(--bs-body-bg-rgb), 0.5);
}
.fullscreen-mode .page-container {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
height: 100vh;
width: 100%;
}
.fullscreen-mode canvas {
max-width: 100%;
max-height: 100vh;
width: 100% !important;
height: auto !important;
margin: 0;
object-fit: contain;
}
.fullscreen-mode .btn-group {
display: flex;
align-items: center;
gap: 4px;
}
.fullscreen-mode .btn-group .btn {
min-width: 44px;
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
}
.fullscreen-mode .btn-group .btn.disabled {
min-width: 120px;
padding: 6px 12px;
}
/* Mobile-specific adjustments */
@media (max-width: 768px) {
.fullscreen-mode .controls {
bottom: 5px;
padding: 4px 8px;
width: auto;
max-width: calc(100% - 20px);
}
.fullscreen-mode .btn-group .btn {
min-width: 36px;
padding: 4px 6px;
}
.fullscreen-mode .btn-group .btn.disabled {
min-width: 110px;
padding: 4px 8px;
}
.fullscreen-mode #pageNum,
.fullscreen-mode #pageCount {
font-size: 0.875rem;
margin: 0 2px;
}
.fullscreen-mode .btn-group {
gap: 3px;
}
/* Add spacing between page numbers and "of" text */
.fullscreen-mode .btn-light.disabled span {
margin: 0 2px;
}
}
/* Theme-specific button styles */
[data-bs-theme="light"] .btn-light {
background-color: #e9ecef;
border-color: #dee2e6;
color: #212529;
}
[data-bs-theme="light"] .btn-light:hover {
background-color: #dde2e6;
border-color: #ced4da;
color: #000;
}
[data-bs-theme="dark"] .btn-light {
background-color: #343a40;
border-color: #495057;
color: #f8f9fa;
}
[data-bs-theme="dark"] .btn-light:hover {
background-color: #495057;
border-color: #6c757d;
color: #fff;
}
/* Button group specific styles */
.btn-group .btn-light {
margin: 0 1px;
}
.btn-group .btn-light.disabled {
opacity: 0.8;
}
.pdf-workspace { padding:16px; }
#pdfContainer { position:relative; width:100%; min-height:380px; display:flex; flex-direction:column; align-items:center; padding:16px 0; overflow:auto; background:var(--bg); border:1px solid var(--line); border-radius:8px; }
#loadingMessage { position:absolute; inset:0; background:var(--bg); z-index:1; color:var(--muted); }
.page-container { background:white; box-shadow:0 4px 20px #0002; max-width:100%; overflow:hidden; }
.page-container canvas { display:block; }
.controls { display:flex; justify-content:center; margin-top:16px; }
.controls .btn-group { flex-wrap:wrap; justify-content:center; gap:6px; }
.controls .btn { font-size:12px; }
.actionButton { display:flex; justify-content:center; margin-top:12px; }
.actionButton .btn { margin:0; }
.pdf-workspace.fullscreen-mode { position:fixed; inset:0; width:100%; height:100dvh; z-index:9999; background:var(--bg); padding:0; border:0; border-radius:0; display:flex; flex-direction:column; }
.fullscreen-mode #pdfContainer { flex:1; height:100%; min-height:0; padding:0; border:0; border-radius:0; justify-content:center; }
.fullscreen-mode .controls { position:absolute; bottom:12px; left:50%; transform:translateX(-50%); margin:0; padding:8px; border:1px solid var(--line); border-radius:10px; background:var(--surface); max-width:calc(100% - 24px); width:max-content; z-index:2; }
.fullscreen-mode .actionButton { display:none; }
@media(max-width:600px) { .pdf-workspace { padding:10px; } .controls .btn { padding:7px 10px; } }
+18 -124
View File
@@ -1,124 +1,18 @@
/* Layout */
body {
min-height: 100vh;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
footer {
margin-top: auto;
padding: 20px;
border-top: 1px solid var(--bs-border-color);
width: 100%;
}
/* Monitor Cards */
.monitor-card {
transition: transform 0.2s ease-in-out;
height: 100%;
}
.monitor-card:hover {
transform: translateY(-5px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.monitor-card .card-body {
display: flex;
flex-direction: column;
justify-content: center;
padding: 1.5rem;
min-height: 320px;
}
.monitor-header {
background-color: var(--bs-body-bg);
border-bottom: 1px solid var(--bs-border-color);
}
.progress {
background-color: var(--bs-tertiary-bg);
}
/* CPU Gauge */
.cpu-gauge {
position: relative;
width: 100%;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.cpu-gauge canvas {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.cpu-gauge .position-relative {
position: absolute !important;
top: 50%;
left: 50%;
transform: translate(-50%, -55%);
z-index: 2;
width: auto;
text-align: center;
line-height: 1.2;
}
.cpu-gauge .h3 {
font-size: 1.75rem;
line-height: 1;
margin: 0;
}
.cpu-gauge .small {
opacity: 0.75;
margin-top: 3px;
}
/* Theme & Logo */
.hover-effect {
transition: opacity 0.3s ease;
}
.hover-effect:hover {
opacity: 0.8;
}
.theme-light .dark-logo {
display: none;
}
.theme-dark .light-logo {
display: none;
}
/* Grid Spacing */
.row.g-4 {
--bs-gutter-y: 2rem;
}
/* Responsive */
@media (max-width: 768px) {
.container {
padding: 0.5rem;
}
.h4 {
font-size: 1.25rem;
}
.monitor-card .card-body {
padding: 1rem;
min-height: 280px;
}
.row.g-4 > [class*="col-"] {
margin-bottom: 1rem;
}
}
.stats-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:22px; }
.stats-cell { min-width:0; }
.monitor-card { padding:0; overflow:hidden; box-shadow:var(--shadow); }
.monitor-card .card-header { padding:18px 22px; background:var(--surface); border-bottom:1px solid var(--line); }
.monitor-card h3 { font-size:14px; }
.monitor-card .card-body { padding:24px; min-height:235px; }
.monitor-card .card-body > .text-muted { overflow-wrap:anywhere; }
.monitor-card .row { flex-wrap:nowrap; gap:10px; }
.monitor-card .col-4 { min-width:0; }
.monitor-card .fw-bold { font-size:13px; overflow-wrap:anywhere; margin-top:6px; }
.monitor-card .small { font-size:11px; }
.cpu-gauge { min-height:185px; display:grid; place-items:center; }
.raid-summary { text-align:center; padding:20px 0; }
.raid-summary > .ui-icon { font-size:30px; margin-bottom:12px; }
.monitor-card .progress { margin-top:8px; }
.monitor-card .resource-section + .resource-section { margin-top:24px; }
@media(max-width:1000px) { .stats-grid { grid-template-columns:1fr; } }
@media(max-width:600px) { .monitor-card .card-body { padding:18px; } .monitor-card .row { gap:6px; } .monitor-card .p-2 { padding:10px 6px; } }
+11 -86
View File
@@ -1,86 +1,11 @@
html, body {
height: 100%;
margin: 0;
overflow-x: hidden;
}
.custom-container {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
padding: 0;
max-width: 100%;
}
.media-container {
display: flex;
justify-content: center;
align-items: center;
max-width: 100%;
max-height: 80vh;
margin: 0 auto;
overflow: visible;
flex-direction: column;
}
.media-container img {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
.media-container video {
max-width: 100%;
max-height: 70vh;
}
.media-container audio {
width: 100%;
max-width: 600px;
margin: 20px 0;
}
.controls {
margin: 20px 0;
text-align: center;
}
footer {
margin-top: auto;
padding: 20px;
border-top: 1px solid #ddd;
width: 100%;
}
.hover-effect {
transition: opacity 0.3s ease;
}
.hover-effect:hover {
opacity: 0.8;
}
.theme-light .dark-logo {
display: none;
}
.theme-dark .light-logo {
display: none;
}
.file-info table {
table-layout: fixed;
word-wrap: break-word;
}
@media (max-width: 576px) {
.file-info table {
width: 95% !important;
}
.file-info th {
width: 40%;
}
.file-info td {
width: 60%;
}
}
.media-container { display:flex; align-items:center; justify-content:center; flex-wrap:wrap; gap:12px; padding:24px; background:var(--bg); border:1px solid var(--line); border-radius:10px; min-height:200px; }
.media-container img,.media-container video { display:block; max-width:100%; max-height:65vh; object-fit:contain; flex-basis:100%; }
.media-container audio { display:block; width:100%; max-width:680px; margin:28px auto; flex-basis:100%; }
.media-container .btn { margin:0; }
.file-info { max-width:760px; }
.file-info h4 { font-size:16px; }
.file-info table { width:100%; table-layout:fixed; }
.file-info th { width:160px; }
.file-info td { overflow-wrap:anywhere; font-size:12px; }
.media-page .input-group { flex-wrap:wrap; } .media-page .input-group input { flex:1; min-width:160px; }
@media(max-width:600px) { .media-container { padding:12px; } .file-info th { width:110px; } }
+20 -7
View File
@@ -1,26 +1,35 @@
<?php
require_once __DIR__ . '/../../includes/auth.php';
requireLogin('../../index.php');
requirePermission('delPer', 'You do not have permission to perform this operation.');
require_once __DIR__ . '/../../config/config.php';
$sftp = initializeSFTP();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete'])) {
$itemToDelete = $_POST['delete'];
$parentDir = dirname($itemToDelete);
$itemToDelete = requireRemotePath($_POST['delete'], $defaultPath);
if ($itemToDelete === $defaultPath) {
http_response_code(400);
exit('Cannot delete the workspace root.');
}
$parentPath = remoteDirname($itemToDelete);
if ($parentDir === '/' || $parentDir === '.') {
$parentDir = $defaultPath;
if ($parentPath === '/' || $parentPath === '.') {
$parentPath = $defaultPath;
}
if ($sftp->is_dir($itemToDelete)) {
function deleteFolder($sftp, $folderPath) {
$items = $sftp->nlist($folderPath);
if ($items === false) return false;
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$itemPath = $folderPath . '/' . $item;
if ($sftp->is_dir($itemPath)) {
deleteFolder($sftp, $itemPath);
if (!deleteFolder($sftp, $itemPath)) return false;
} else {
$sftp->delete($itemPath);
if (!$sftp->delete($itemPath)) return false;
}
}
return $sftp->rmdir($folderPath);
@@ -31,7 +40,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete'])) {
$success = $sftp->delete($itemToDelete);
}
header("Location: index.php?path=" . urlencode($parentDir));
if (!$success) {
http_response_code(500);
exit('Failed to delete the item.');
}
header("Location: index.php?path=" . urlencode($parentPath));
exit;
}
+58 -59
View File
@@ -1,10 +1,10 @@
<?php
set_time_limit(900); // 15 minutes max execution time
require_once __DIR__ . '/../../includes/auth.php';
requireLogin('../../index.php');
requirePermission('downPer', 'You do not have permission to perform this operation.');
session_write_close();
// Include this at the top to see potential errors
// Comment out in production
ini_set('display_errors', 0);
error_reporting(0);
set_time_limit(900); // 15 minutes max execution time
ob_start();
@@ -18,12 +18,12 @@ try {
try {
$sftp = initializeSFTP();
file_put_contents($logFile, "SFTP connection established\n", FILE_APPEND);
} catch (Exception $e) {
file_put_contents($logFile, "SFTP connection failed: " . $e->getMessage() . "\n", FILE_APPEND);
throw new Exception("Failed to connect to SFTP server: " . $e->getMessage());
} catch (Exception $exception) {
file_put_contents($logFile, "SFTP connection failed: " . $exception->getMessage() . "\n", FILE_APPEND);
throw new Exception("Failed to connect to SFTP server: " . $exception->getMessage());
}
function zipFolderRecursive($sftp, $remoteBasePath, $currentPath, $zip, $logFile, &$tempFiles) {
function zipFolderRecursive($sftp, $remoteBasePath, $currentPath, $zip, $logFile, &$temporaryFiles) {
file_put_contents($logFile, "Processing directory: $currentPath\n", FILE_APPEND);
$files = $sftp->nlist($currentPath);
@@ -35,36 +35,35 @@ try {
file_put_contents($logFile, "Found " . count($files) . " items in $currentPath\n", FILE_APPEND);
foreach ($files as $file) {
if ($file == '.' || $file == '..') continue;
foreach ($files as $fileName) {
if ($fileName == '.' || $fileName == '..') continue;
$fullRemotePath = rtrim($currentPath, '/') . '/' . $file;
$remotePath = rtrim($currentPath, '/') . '/' . $fileName;
$baseDirName = basename($remoteBasePath);
$relPathFromBase = substr($fullRemotePath, strlen(dirname($remoteBasePath)) + 1);
file_put_contents($logFile, "Processing: $fullRemotePath (relative: $relPathFromBase)\n", FILE_APPEND);
$relativePath = substr($remotePath, strlen(rtrim(remoteDirname($remoteBasePath), '/')) + 1);
file_put_contents($logFile, "Processing: $remotePath (relative: $relativePath)\n", FILE_APPEND);
$isDir = $sftp->is_dir($fullRemotePath);
$isDirectory = $sftp->is_dir($remotePath);
if ($isDir) {
file_put_contents($logFile, "Found subdirectory: $fullRemotePath\n", FILE_APPEND);
$zip->addEmptyDir($relPathFromBase);
if ($isDirectory) {
file_put_contents($logFile, "Found subdirectory: $remotePath\n", FILE_APPEND);
$zip->addEmptyDir($relativePath);
zipFolderRecursive($sftp, $remoteBasePath, $fullRemotePath, $zip, $logFile, $tempFiles);
zipFolderRecursive($sftp, $remoteBasePath, $remotePath, $zip, $logFile, $temporaryFiles);
} else {
$localTempFile = tempnam(sys_get_temp_dir(), 'sftp');
$tempFiles[] = $localTempFile;
file_put_contents($logFile, "Downloading to temp file: $localTempFile\n", FILE_APPEND);
$temporaryFile = tempnam(sys_get_temp_dir(), 'sftp');
$temporaryFiles[] = $temporaryFile;
file_put_contents($logFile, "Downloading to temp file: $temporaryFile\n", FILE_APPEND);
$downloadStart = time();
$downloadSuccess = $sftp->get($fullRemotePath, $localTempFile);
$downloadSuccess = $sftp->get($remotePath, $temporaryFile);
$downloadTime = time() - $downloadStart;
if ($downloadSuccess) {
$fileSize = filesize($localTempFile);
$fileSize = filesize($temporaryFile);
file_put_contents($logFile, "Download successful ($downloadTime seconds), size: $fileSize bytes\n", FILE_APPEND);
$zip->addFile($localTempFile, $relPathFromBase);
$zip->addFile($temporaryFile, $relativePath);
} else {
file_put_contents($logFile, "Failed to download file after $downloadTime seconds\n", FILE_APPEND);
}
@@ -83,17 +82,17 @@ try {
}
$tempFiles = [];
$temporaryFiles = [];
try {
zipFolderRecursive($sftp, $folderPath, $folderPath, $zip, $logFile, $tempFiles);
zipFolderRecursive($sftp, $folderPath, $folderPath, $zip, $logFile, $temporaryFiles);
file_put_contents($logFile, "Closing zip file\n", FILE_APPEND);
$zipSuccess = $zip->close();
foreach ($tempFiles as $tempFile) {
if (file_exists($tempFile)) {
@unlink($tempFile);
foreach ($temporaryFiles as $temporaryFile) {
if (file_exists($temporaryFile)) {
@unlink($temporaryFile);
}
}
@@ -104,39 +103,39 @@ try {
file_put_contents($logFile, "Failed to create zip\n", FILE_APPEND);
return false;
}
} catch (Exception $e) {
foreach ($tempFiles as $tempFile) {
if (file_exists($tempFile)) {
@unlink($tempFile);
} catch (Exception $exception) {
foreach ($temporaryFiles as $temporaryFile) {
if (file_exists($temporaryFile)) {
@unlink($temporaryFile);
}
}
file_put_contents($logFile, "Error during zip creation: " . $e->getMessage() . "\n", FILE_APPEND);
throw $e;
file_put_contents($logFile, "Error during zip creation: " . $exception->getMessage() . "\n", FILE_APPEND);
throw $exception;
}
}
if (($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['file'])) ||
($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['file']))) {
$file = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST['file'] : $_GET['file'];
file_put_contents($logFile, "Requested file: $file\n", FILE_APPEND);
$filePath = requireRemotePath($_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST['file'] : $_GET['file'], $defaultPath);
file_put_contents($logFile, "Requested file: $filePath\n", FILE_APPEND);
$fileExists = $sftp->file_exists($file);
$isDir = $sftp->is_dir($file);
$fileExists = $sftp->file_exists($filePath);
$isDirectory = $sftp->is_dir($filePath);
file_put_contents($logFile, "File exists: " . ($fileExists ? "Yes" : "No") . "\n", FILE_APPEND);
file_put_contents($logFile, "Is directory: " . ($isDir ? "Yes" : "No") . "\n", FILE_APPEND);
file_put_contents($logFile, "Is directory: " . ($isDirectory ? "Yes" : "No") . "\n", FILE_APPEND);
if (!$fileExists && !$isDir) {
throw new Exception("File not found: $file");
if (!$fileExists && !$isDirectory) {
throw new Exception("File not found: $filePath");
}
if ($isDir) {
if ($isDirectory) {
$zipFilePath = tempnam(sys_get_temp_dir(), 'folder_') . '.zip';
file_put_contents($logFile, "Creating zip at: $zipFilePath\n", FILE_APPEND);
if (zipFolder($sftp, $file, $zipFilePath, $logFile)) {
if (zipFolder($sftp, $filePath, $zipFilePath, $logFile)) {
if (file_exists($zipFilePath) && filesize($zipFilePath) > 0) {
file_put_contents($logFile, "Sending zip file to browser, size: " . filesize($zipFilePath) . " bytes\n", FILE_APPEND);
@@ -145,7 +144,7 @@ try {
}
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . basename($file) . '.zip"');
header('Content-Disposition: attachment; filename="' . basename($filePath) . '.zip"');
header('Content-Length: ' . filesize($zipFilePath));
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
@@ -161,19 +160,19 @@ try {
throw new Exception("Failed to create zip archive");
}
} else {
$localFile = basename($file);
file_put_contents($logFile, "Downloading single file: $localFile\n", FILE_APPEND);
$fileName = basename($filePath);
file_put_contents($logFile, "Downloading single file: $fileName\n", FILE_APPEND);
$tempFile = tempnam(sys_get_temp_dir(), 'file_');
$temporaryFile = tempnam(sys_get_temp_dir(), 'file_');
$downloadStart = time();
$downloadSuccess = $sftp->get($file, $tempFile);
$downloadSuccess = $sftp->get($filePath, $temporaryFile);
$downloadTime = time() - $downloadStart;
file_put_contents($logFile, "Download " . ($downloadSuccess ? "successful" : "failed") . " ($downloadTime seconds)\n", FILE_APPEND);
if ($downloadSuccess) {
$fileSize = filesize($tempFile);
$fileSize = filesize($temporaryFile);
file_put_contents($logFile, "Downloaded file size: $fileSize bytes\n", FILE_APPEND);
if ($fileSize > 0) {
@@ -182,29 +181,29 @@ try {
}
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $localFile . '"');
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Content-Length: ' . $fileSize);
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
readfile($tempFile);
readfile($temporaryFile);
file_put_contents($logFile, "Download completed\n", FILE_APPEND);
@unlink($tempFile);
@unlink($temporaryFile);
exit;
} else {
@unlink($tempFile);
@unlink($temporaryFile);
throw new Exception("Downloaded file is empty");
}
} else {
@unlink($tempFile);
@unlink($temporaryFile);
throw new Exception("Failed to download file from SFTP server");
}
}
} else {
throw new Exception("Invalid request method or missing file parameter");
}
} catch (Exception $e) {
$errorMessage = "Error: " . $e->getMessage();
} catch (Exception $exception) {
$errorMessage = "Error: " . $exception->getMessage();
file_put_contents($logFile, $errorMessage . "\n", FILE_APPEND);
while (ob_get_level()) {
+24 -37
View File
@@ -1,20 +1,7 @@
<?php
session_start();
if(!isset($_SESSION['uname'])){
header("location: ../../index.php");
session_destroy();
exit;
}
// Check if user has permission to upload/modify
if(!isset($_SESSION["upPer"]) || $_SESSION["upPer"] != true) {
die("You don't have permission to extract archives.");
}
// Include error reporting for debugging
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../includes/auth.php';
requireLogin('../../index.php');
requirePermission('upPer', 'You do not have permission to perform this operation.');
require_once __DIR__ . '/../../config/config.php';
use phpseclib3\Net\SFTP;
@@ -23,21 +10,21 @@ $sftp = initializeSFTP();
// Process extraction request
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['archive'])) {
$archivePath = $_POST['archive'];
$dirPath = dirname($archivePath);
$archivePath = requireRemotePath($_POST['archive'], $defaultPath);
$directoryPath = remoteDirname($archivePath);
$archiveName = basename($archivePath);
$extension = pathinfo($archiveName, PATHINFO_EXTENSION);
// Create a temporary local directory for extraction
$tempDir = sys_get_temp_dir() . '/sftp_extract_' . time();
if (!mkdir($tempDir, 0777, true)) {
$temporaryDirectory = sys_get_temp_dir() . '/sftp_extract_' . time();
if (!mkdir($temporaryDirectory, 0777, true)) {
die("Failed to create temporary directory");
}
// Download the archive to the temp directory
$localArchivePath = $tempDir . '/' . $archiveName;
$localArchivePath = $temporaryDirectory . '/' . $archiveName;
if (!$sftp->get($archivePath, $localArchivePath)) {
rmdir($tempDir);
rmdir($temporaryDirectory);
die("Failed to download the archive");
}
@@ -48,11 +35,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['archive'])) {
// Determine the extraction method based on file extension
switch (strtolower($extension)) {
case 'zip':
$extractionSuccess = extractZip($localArchivePath, $tempDir);
$extractionSuccess = extractZip($localArchivePath, $temporaryDirectory);
break;
case 'rar':
$extractionSuccess = extractRar($localArchivePath, $tempDir);
$extractionSuccess = extractRar($localArchivePath, $temporaryDirectory);
break;
case 'tar':
@@ -60,7 +47,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['archive'])) {
case 'bz2':
case 'xz':
case '7z':
$extractionSuccess = extractArchive($localArchivePath, $tempDir);
$extractionSuccess = extractArchive($localArchivePath, $temporaryDirectory);
break;
default:
@@ -69,20 +56,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['archive'])) {
if ($extractionSuccess) {
// Upload extracted files back to the server
uploadExtractedFiles($sftp, $tempDir, $dirPath);
uploadExtractedFiles($sftp, $temporaryDirectory, $directoryPath);
// Clean up temporary directory
deleteDirectory($tempDir);
deleteDirectory($temporaryDirectory);
// Redirect back to the file listing
header("Location: index.php?path=" . urlencode($dirPath));
header("Location: index.php?path=" . urlencode($directoryPath));
exit;
} else {
die("Failed to extract the archive");
}
} catch (Exception $e) {
deleteDirectory($tempDir);
die("Error during extraction: " . $e->getMessage());
} catch (Exception $exception) {
deleteDirectory($temporaryDirectory);
die("Error during extraction: " . $exception->getMessage());
}
} else {
header("Location: index.php");
@@ -140,7 +127,7 @@ function extractArchive($archivePath, $destination) {
$command = '';
// Change to the destination directory
$currentDir = getcwd();
$originalDirectory = getcwd();
chdir($destination);
if (PHP_OS_FAMILY === 'Windows') {
@@ -189,20 +176,20 @@ function extractArchive($archivePath, $destination) {
exec($command, $output, $returnCode);
// Change back to the original directory
chdir($currentDir);
chdir($originalDirectory);
return $returnCode === 0;
}
// Upload extracted files back to the SFTP server
function uploadExtractedFiles($sftp, $localDir, $remoteDir) {
$items = scandir($localDir);
function uploadExtractedFiles($sftp, $localDirectory, $remoteDirectory) {
$items = scandir($localDirectory);
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$localPath = $localDir . '/' . $item;
$remotePath = $remoteDir . '/' . $item;
$localPath = $localDirectory . '/' . $item;
$remotePath = $remoteDirectory . '/' . $item;
if (is_dir($localPath)) {
// Create the directory on the remote server
+106 -141
View File
@@ -1,85 +1,70 @@
<?php
session_start();
require_once __DIR__ . '/../../includes/auth.php';
requireLogin('../../index.php');
if(!isset($_SESSION['uname'])){
header("location: ../../index.php");
session_destroy();
exit;
}
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../includes/workspace.php';
$sftp = initializeSFTP();
// Include this at the top to see potential errors
// Comment out in production
ini_set('display_errors', 1);
error_reporting(E_ALL);
$currentPath = isset($_GET['path']) ? normalizePath($_GET['path']) : $defaultPath;
if (!isRemotePathWithin($currentPath, $defaultPath)) {
$currentPath = $defaultPath;
}
require_once __DIR__ . '/../../config/config.php';
$sftp = initializeSFTP();
if (!$sftp->chdir($currentPath)) {
die("Failed to navigate to the selected folder: $currentPath");
}
$currentPath = isset($_GET['path']) ? normalizePath($_GET['path']) : $defaultPath;
if (strpos($currentPath, $defaultPath) !== 0) {
$currentPath = $defaultPath;
}
$items = $sftp->nlist();
if ($items === false) {
http_response_code(500);
exit('Failed to list directory contents.');
}
if (!$sftp->chdir($currentPath)) {
die("Failed to navigate to the selected folder: $currentPath");
}
$items = $sftp->nlist();
$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
$videoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv'];
$audioExtensions = ['mp3', 'wav', 'm4a', 'flac', 'aac'];
$editableExtensions = ['txt', 'html', 'css', 'js', 'php', 'xml', 'json', 'md', 'csv', 'log', 'ini', 'conf', 'sh', 'bat', 'py', 'rb', 'java', 'c', 'cpp', 'h', 'hpp'];
$archiveExtensions = ['zip', 'rar', 'tar', 'gz', '7z', 'bz2', 'xz', 'tar.gz', 'tar.bz2', 'tar.xz'];
$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
$videoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv'];
$audioExtensions = ['mp3', 'wav', 'm4a', 'flac', 'aac'];
$editableExtensions = ['txt', 'html', 'css', 'js', 'php', 'xml', 'json', 'md', 'csv', 'log', 'ini', 'conf', 'sh', 'bat', 'py', 'rb', 'java', 'c', 'cpp', 'h', 'hpp'];
$archiveExtensions = ['zip', 'rar', 'tar', 'gz', '7z', 'bz2', 'xz', 'tar.gz', 'tar.bz2', 'tar.xz'];
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title>FTP</title>
<title>Files · USB RAID</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../../css/bootstrap.css">
<link rel="stylesheet" href="../../css/modern.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="css/index.css">
<script src="../../js/bootstrap.bundle.js"></script>
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/modern.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/workspace.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'content/ftp/css/index.css') ?>">
<script src="<?= workspaceAsset('../../', 'js/theme.js') ?>"></script>
<script src="<?= workspaceAsset('../../', 'js/ui.js') ?>" defer></script>
</head>
<body class="text-center">
<div class="d-flex justify-content-end p-3">
<button id="themeToggle" class="btn btn-sm theme-toggle">
<i class="bi"></i>
<span id="themeText"></span>
</button>
</div>
<div class="custom-container dark-box">
<header class="row border-bottom m-5">
<h1>USB RAID Array</h1>
<div class="mb-3 p-3">
<a href="../logout.php" class="btn btn-danger">Logout</a>
<a href="../changepassword.php" class="btn btn-warning">Change Password</a>
<?php if (isset($_SESSION["admin"]) && $_SESSION["admin"] == true) { ?>
<a href="../adminpanel.php" class="btn btn-primary">Admin Panel</a>
<a href="serverstat.php" class="btn btn-primary">Server Status</a>
<?php } ?>
</div>
</header>
<section class="row">
<article class="col-8 border border-2 border-primary rounded p-2">
<div class="col">
<h4>Current Path: <?= htmlspecialchars($currentPath) ?></h4>
</div>
<div class="col">
<?php renderWorkspaceStart([
'root' => '../../',
'active' => 'files',
'section' => 'Files',
'eyebrow' => 'Your remote storage',
'title' => 'All files',
'description' => 'Everything in its place. Browse, manage and transfer your files.',
'connected' => true,
]); ?>
<nav class="pathbar" aria-label="Directory path"><a href="index.php" title="Home directory">⌂ <span>Home</span></a><?php
$relativePath = trim(substr($currentPath, strlen(rtrim($defaultPath, '/'))), '/');
$crumbPath = rtrim($defaultPath, '/');
foreach (array_filter(explode('/', $relativePath), 'strlen') as $segment):
$crumbPath .= '/' . $segment;
?><span class="path-separator" aria-hidden="true">/</span><a href="?path=<?= urlencode($crumbPath) ?>"><?= htmlspecialchars($segment) ?></a><?php endforeach; ?><span class="path-label" title="<?= htmlspecialchars($currentPath) ?>"><?= htmlspecialchars($currentPath) ?></span></nav>
<section class="file-panel" aria-label="File browser">
<div class="browser-toolbar"><label class="search-field"><span aria-hidden="true">⌕</span><input id="fileSearch" type="search" placeholder="Search this folder…" aria-label="Search this folder"><kbd>/</kbd></label><select id="typeFilter" class="form-select" aria-label="Filter by type"><option value="all">All types</option><option value="folder">Folders</option><option value="file">Files</option></select></div>
<div class="file-content">
<!-- Action Buttons -->
<div class="action-buttons">
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) { ?>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createFileModal">Create File</button>
<button type="button" class="btn btn-success" onclick="document.getElementById('fileInput').click()">Upload Files</button>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createDirModal">Create Directory</button>
<button type="button" class="btn btn-info" onclick="document.getElementById('dirInput').click()">Upload Directory</button>
<?php if (hasPermission('upPer')) { ?>
<button type="button" class="btn" data-dialog="createFileModal">+ New file</button>
<button type="button" class="btn btn-primary" onclick="document.getElementById('fileInput').click()">↑ Upload files</button>
<button type="button" class="btn" data-dialog="createDirModal">+ New folder</button>
<button type="button" class="btn btn-info" onclick="document.getElementById('dirInput').click()">Upload folder</button>
<input type="file" id="fileInput" multiple style="display: none;" onchange="handleFileSelect(event)">
<input type="file" id="dirInput" webkitdirectory directory multiple style="display: none;" onchange="handleFileSelect(event)">
<?php } ?>
@@ -87,57 +72,57 @@
<!-- Modal Forms -->
<!-- Create Directory Dialog -->
<div class="modal fade" id="createDirModal" tabindex="-1" aria-labelledby="createDirModalLabel" aria-hidden="true">
<dialog class="modal" id="createDirModal" tabindex="-1" aria-labelledby="createDirModalLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createDirModalLabel">Create New Directory</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="btn-close" data-dialog-close aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="dirName" class="form-label">Directory Name</label>
<input type="text" class="form-control" id="dirName" placeholder="Enter directory name">
<input type="text" class="form-control" id="dirName" autofocus placeholder="Enter directory name">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-secondary" data-dialog-close>Cancel</button>
<button type="button" class="btn btn-primary" onclick="createDirectory()">Create</button>
</div>
</div>
</div>
</div>
</dialog>
<!-- Create File Dialog -->
<div class="modal fade" id="createFileModal" tabindex="-1" aria-labelledby="createFileModalLabel" aria-hidden="true">
<dialog class="modal" id="createFileModal" tabindex="-1" aria-labelledby="createFileModalLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createFileModalLabel">Create New File</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="btn-close" data-dialog-close aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="fileName" class="form-label">File Name (with extension)</label>
<input type="text" class="form-control" id="fileName" placeholder="example.txt">
<input type="text" class="form-control" id="fileName" autofocus placeholder="example.txt">
<div class="form-text">Supported extensions: .txt, .html, .css, .js, .php, etc.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-secondary" data-dialog-close>Cancel</button>
<button type="button" class="btn btn-primary" onclick="createFile()">Create</button>
</div>
</div>
</div>
</div>
</dialog>
<!-- Rename Confirmation Dialog -->
<div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModalLabel" aria-hidden="true">
<dialog class="modal" id="renameModal" tabindex="-1" aria-labelledby="renameModalLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="renameModalLabel">Rename Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="btn-close" data-dialog-close aria-label="Close"></button>
</div>
<form method="POST" action="rename.php">
<div class="modal-body">
@@ -149,31 +134,31 @@
</div>
<div class="mb-3">
<label for="newName" class="form-label">New Name:</label>
<input type="text" class="form-control" id="newName" name="newName" required>
<input type="text" class="form-control" id="newName" autofocus name="newName" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-secondary" data-dialog-close>Cancel</button>
<button type="submit" class="btn btn-primary">Rename</button>
</div>
</form>
</div>
</div>
</div>
</dialog>
<!-- End of Modal Forms -->
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) { ?>
<?php if (hasPermission('upPer')) { ?>
<div class="dropzone" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleDrop(event)">
Drop files or folders here to upload
<span class="upload-symbol" aria-hidden="true">↑</span><span><strong>Drop files or folders here</strong><small>Or use the upload buttons to browse your device</small></span>
</div>
<?php } ?>
<table>
<div class="file-table-scroll"><table class="file-table">
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th>Actions</th>
<th scope="col">Name</th>
<th scope="col">Size</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
@@ -197,31 +182,30 @@
if ($currentPath !== $defaultPath) : ?>
<tr>
<td colspan="3"><a class="text-danger" href="?path=<?= urlencode(dirname($currentPath)) ?>"><b>.. (Go Back)</b></a></td>
<td colspan="3"><a class="text-danger" href="?path=<?= urlencode(remoteDirname($currentPath)) ?>"><b>↑ Parent folder</b></a></td>
</tr>
<?php endif;
foreach ($directories as $directory) : ?>
<tr>
<td colspan="2">
<a href="?path=<?= urlencode($currentPath . '/' . $directory) ?>"><?= htmlspecialchars($directory) ?>/</a>
</td>
<tr data-file-name="<?= htmlspecialchars($directory) ?>" data-kind="folder">
<td><span class="file-icon folder-icon" aria-hidden="true">▰</span>
<a href="?path=<?= urlencode(rtrim($currentPath, '/') . '/' . $directory) ?>"><?= htmlspecialchars($directory) ?></a></td><td class="size-cell">—</td>
<td>
<?php if (isset($_SESSION["delPer"]) && $_SESSION["delPer"] == true) : ?>
<?php if (hasPermission('delPer')) : ?>
<form method="POST" action="delete.php" style="display:inline;">
<input type="hidden" name="delete" value="<?= htmlspecialchars($currentPath . '/' . $directory) ?>">
<input type="hidden" name="delete" value="<?= htmlspecialchars(rtrim($currentPath, '/') . '/' . $directory) ?>">
<button type="submit" class="btn btn-danger mt-1 mb-1" onclick="confirmDelete(event)">Delete</button>
</form>
<?php endif; ?>
<?php if (isset($_SESSION["downPer"]) && $_SESSION["downPer"] == true) : ?>
<?php if (hasPermission('downPer')) : ?>
<form method="POST" action="download.php" style="display:inline;">
<input type="hidden" name="file" value="<?= htmlspecialchars($currentPath . '/' . $directory) ?>">
<input type="hidden" name="file" value="<?= htmlspecialchars(rtrim($currentPath, '/') . '/' . $directory) ?>">
<button type="submit" class="btn btn-success mt-1 mb-1">Download</button>
</form>
<?php endif; ?>
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) : ?>
<button type="button" class="btn btn-warning mt-1 mb-1" onclick="openRenameModal('<?= htmlspecialchars(addslashes($directory)) ?>', '<?= htmlspecialchars(addslashes($currentPath . '/' . $directory)) ?>')">Rename</button>
<?php if (hasPermission('upPer')) : ?>
<button type="button" class="btn btn-warning mt-1 mb-1" onclick="openRenameModal('<?= htmlspecialchars(addslashes($directory)) ?>', '<?= htmlspecialchars(addslashes(rtrim($currentPath, '/') . '/' . $directory)) ?>', true)">Rename</button>
<?php endif; ?>
</td>
</tr>
@@ -230,8 +214,8 @@
foreach ($files as $file) : ?>
<?php
$fileExtension = pathinfo($file, PATHINFO_EXTENSION);
$isPDF = strtolower($fileExtension) === 'pdf';
$fileExtension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
$isPdf = $fileExtension === 'pdf';
$isImage = in_array($fileExtension, $imageExtensions);
$isVideo = in_array($fileExtension, $videoExtensions);
$isAudio = in_array($fileExtension, $audioExtensions);
@@ -242,11 +226,10 @@
$isArchive = true;
}
?>
<tr>
<td>
<tr data-file-name="<?= htmlspecialchars($file) ?>" data-kind="file">
<td><span class="file-icon" aria-hidden="true">▤</span>
<?php if ($isMedia): ?>
<a class="text-info-emphasis" href="view.php?file=<?= urlencode($currentPath . '/' . $file) ?>">
<a class="text-info-emphasis" href="view.php?file=<?= urlencode(rtrim($currentPath, '/') . '/' . $file) ?>">
<?= htmlspecialchars($file) ?>
</a>
<?php if ($isImage): ?>
@@ -257,12 +240,12 @@
<span class="badge bg-info rounded-pill">Audio</span>
<?php endif; ?>
<?php elseif ($isEditable): ?>
<a class="text-warning-emphasis" href="open.php?file=<?= urlencode($currentPath . '/' . $file) ?>">
<a class="text-warning-emphasis" href="open.php?file=<?= urlencode(rtrim($currentPath, '/') . '/' . $file) ?>">
<?= htmlspecialchars($file) ?>
</a>
<span class="badge bg-secondary rounded-pill"><?= htmlspecialchars($fileExtension) ?></span>
<?php elseif ($isPDF): ?>
<a class="text-info-emphasis" href="pdf.php?file=<?= urlencode($currentPath . '/' . $file) ?>&type=pdf">
<?php elseif ($isPdf): ?>
<a class="text-info-emphasis" href="pdf.php?file=<?= urlencode(rtrim($currentPath, '/') . '/' . $file) ?>&type=pdf">
<?= htmlspecialchars($file) ?>
</a>
<span class="badge bg-danger rounded-pill">PDF</span>
@@ -272,28 +255,28 @@
<?php endif; ?>
</td>
<td>
<span class="text"><?= formatBytes($sftp->stat($currentPath . '/' . $file)['size']) ?></span>
<span class="text"><?= formatBytes($sftp->stat(rtrim($currentPath, '/') . '/' . $file)['size']) ?></span>
</td>
<td>
<?php if (isset($_SESSION["delPer"]) && $_SESSION["delPer"] == true) : ?>
<?php if (hasPermission('delPer')) : ?>
<form method="POST" action="delete.php" style="display:inline;">
<input type="hidden" name="delete" value="<?= htmlspecialchars($currentPath . '/' . $file) ?>">
<input type="hidden" name="delete" value="<?= htmlspecialchars(rtrim($currentPath, '/') . '/' . $file) ?>">
<button type="submit" class="btn btn-danger mt-1 mb-1" onclick="confirmDelete(event)">Delete</button>
</form>
<?php endif; ?>
<?php if (isset($_SESSION["downPer"]) && $_SESSION["downPer"] == true) : ?>
<?php if (hasPermission('downPer')) : ?>
<form method="POST" action="download.php" style="display:inline;">
<input type="hidden" name="file" value="<?= htmlspecialchars($currentPath . '/' . $file) ?>">
<input type="hidden" name="file" value="<?= htmlspecialchars(rtrim($currentPath, '/') . '/' . $file) ?>">
<button type="submit" class="btn btn-success mt-1 mb-1">Download</button>
</form>
<?php endif; ?>
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) : ?>
<button type="button" class="btn btn-warning mt-1 mb-1" onclick="openRenameModal('<?= htmlspecialchars(addslashes($file)) ?>', '<?= htmlspecialchars(addslashes($currentPath . '/' . $file)) ?>')">Rename</button>
<?php if (hasPermission('upPer')) : ?>
<button type="button" class="btn btn-warning mt-1 mb-1" onclick="openRenameModal('<?= htmlspecialchars(addslashes($file)) ?>', '<?= htmlspecialchars(addslashes(rtrim($currentPath, '/') . '/' . $file)) ?>')">Rename</button>
<?php endif; ?>
<?php if ($isArchive && isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) : ?>
<?php if ($isArchive && hasPermission('upPer')) : ?>
<form method="POST" action="extract.php" style="display:inline;">
<input type="hidden" name="archive" value="<?= htmlspecialchars($currentPath . '/' . $file) ?>">
<input type="hidden" name="archive" value="<?= htmlspecialchars(rtrim($currentPath, '/') . '/' . $file) ?>">
<button type="submit" class="btn btn-info mt-1 mb-1">Extract</button>
</form>
<?php endif; ?>
@@ -301,36 +284,18 @@
</tr>
<?php endforeach; ?>
</tbody>
</table>
</table></div>
<div id="emptySearch" class="empty-state" hidden><span aria-hidden="true">⌕</span><h3>No files to show</h3><p>Try another search or upload files to this folder.</p></div>
<div class="browser-status"><span id="fileCount" role="status"></span><span>Folders first · A–Z</span></div>
</div>
</article>
</section>
<?php renderWorkspaceEnd(); ?>
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3 m-3">
<span class="text-muted">Developed by Michal Sedlák</span>
<div class="d-flex gap-3">
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
<img src="../../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
</a>
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
<img src="../../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
</a>
</div>
</footer>
</div>
<script>
// Keep Bootstrap modals outside the filtered SFTP container. A parent
// backdrop-filter creates a stacking context that can block modal input.
document.querySelectorAll('.modal').forEach(function (modal) {
document.body.appendChild(modal);
});
</script>
<script>
// Pass PHP variables to JavaScript
var currentPath = "<?php echo $currentPath; ?>";
const currentPath = <?= json_encode($currentPath, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
</script>
<script src="js/index.js"></script>
<script src="<?= workspaceAsset('../../', 'content/ftp/js/index.js') ?>"></script>
</body>
</html>
+30 -45
View File
@@ -158,6 +158,10 @@ function uploadFiles(files) {
}
};
uploadStatus.setAttribute('role', 'status');
xhr.upload.addEventListener('progress', event => {
if (event.lengthComputable) uploadStatus.textContent = 'Uploading ' + filesAdded + ' file(s) · ' + Math.round(event.loaded / event.total * 100) + '%';
});
xhr.send(formData);
}
@@ -178,8 +182,7 @@ function createDirectory() {
xhr.onload = () => {
if (xhr.status === 200) {
alert('Directory created successfully!');
const modal = bootstrap.Modal.getInstance(document.getElementById('createDirModal'));
if (modal) modal.hide();
document.getElementById('createDirModal').close();
window.location.reload();
} else {
alert('Failed to create directory: ' + xhr.responseText || xhr.statusText);
@@ -221,8 +224,7 @@ function createFile() {
const response = JSON.parse(xhr.responseText);
if (response.success) {
alert('File created successfully!');
const modal = bootstrap.Modal.getInstance(document.getElementById('createFileModal'));
if (modal) modal.hide();
document.getElementById('createFileModal').close();
window.location.reload();
} else {
alert('Failed to create file: ' + (response.message || 'Unknown error'));
@@ -236,53 +238,36 @@ function createFile() {
};
xhr.send(formData);
}
function openRenameModal(name, path) {
function openRenameModal(name, path, isDirectory = false) {
document.getElementById('originalName').value = name;
document.getElementById('fullPath').value = path;
document.getElementById('isDirectory').value = name.endsWith('/') ? '1' : '0';
document.getElementById('isDirectory').value = isDirectory ? '1' : '0';
document.getElementById('newName').value = name.endsWith('/') ? name.slice(0, -1) : name;
const renameModal = new bootstrap.Modal(document.getElementById('renameModal'));
renameModal.show();
document.getElementById('renameModal').showModal();
}
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.getElementById('themeToggle');
const html = document.documentElement;
const themeText = document.getElementById('themeText');
const themeIcon = themeToggle.querySelector('.bi');
function setTheme(theme) {
html.setAttribute('data-bs-theme', theme);
document.body.classList.remove('theme-light', 'theme-dark');
document.body.classList.add('theme-' + theme);
localStorage.setItem('theme', theme);
if (theme === 'dark') {
themeText.textContent = 'Light Mode';
themeIcon.className = 'bi bi-sun';
themeToggle.classList.remove('btn-dark');
themeToggle.classList.add('btn-light');
} else {
themeText.textContent = 'Dark Mode';
themeIcon.className = 'bi bi-moon';
themeToggle.classList.remove('btn-light');
themeToggle.classList.add('btn-dark');
}
}
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme) {
setTheme(savedTheme);
} else {
setTheme(prefersDark ? 'dark' : 'light');
}
themeToggle.addEventListener('click', function() {
const currentTheme = html.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
// Filtering is local to the current directory; no remote request is needed.
const search = document.getElementById('fileSearch');
const typeFilter = document.getElementById('typeFilter');
function filterFiles() {
const query = search.value.trim().toLocaleLowerCase();
const rows = [...document.querySelectorAll('[data-file-name]')];
let visible = 0;
rows.forEach(row => {
row.hidden = !row.dataset.fileName.toLocaleLowerCase().includes(query) || (typeFilter.value !== 'all' && row.dataset.kind !== typeFilter.value);
if (!row.hidden) visible++;
});
document.getElementById('fileCount').textContent = visible + ' of ' + rows.length + ' items';
document.getElementById('emptySearch').hidden = visible !== 0;
}
search?.addEventListener('input', filterFiles);
typeFilter?.addEventListener('change', filterFiles);
if (search) filterFiles();
document.addEventListener('keydown', event => {
if (event.key === '/' && !event.ctrlKey && !event.metaKey && !event.altKey && !event.target.closest('input, textarea, select, [contenteditable], dialog')) {
event.preventDefault();
search?.focus();
}
});
+108
View File
@@ -0,0 +1,108 @@
<?php
// Pure parsers are kept separate so unavailable or malformed SSH output can be tested.
function getDiskData($ssh, $path) {
// The command runs on Linux even when PHP runs on Windows. escapeshellarg()
// follows the local OS, so quote explicitly for the remote POSIX shell.
$quotedPath = "'" . str_replace("'", "'\\''", $path) . "'";
return parseDiskData($ssh->exec('LC_ALL=C df -Pk -- ' . $quotedPath . ' 2>/dev/null'));
}
function getSystemData($ssh, $storagePath) {
$meminfo = $ssh->exec("LC_ALL=C free -m | awk '/^Mem:/{printf \"%s,%s,%s,\", \$2,\$3,\$7} /^Swap:/{printf \"%s,%s,%s\", \$2,\$3,\$4}'");
$cpuIdle = $ssh->exec("LC_ALL=C vmstat 1 2 | tail -1 | awk '{print \$15}'");
return [
'storage' => [
'system' => getDiskData($ssh, '/'),
'data' => getDiskData($ssh, $storagePath),
],
'raid' => parseRaidStatus($ssh->exec('cat /proc/mdstat 2>/dev/null')),
'resources' => parseSystemStats($cpuIdle, $meminfo),
'lastUpdate' => date('Y-m-d H:i:s'),
];
}
function parseDiskData($output) {
$unavailable = ['available_data' => false, 'size' => 'N/A', 'used' => 'N/A', 'available' => 'N/A', 'usage' => 'N/A', 'filesystem' => ''];
if (!is_string($output)) return $unavailable;
foreach (preg_split('/\r?\n/', trim($output)) as $line) {
// POSIX df -Pk gives one filesystem per line and sizes in KiB.
if (!preg_match('/^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.+)$/', trim($line), $parts)) continue;
return [
'available_data' => true,
'filesystem' => $parts[1],
'size' => formatBytes((float) $parts[2] * 1024),
'used' => formatBytes((float) $parts[3] * 1024),
'available' => formatBytes((float) $parts[4] * 1024),
'usage' => $parts[5] . '%',
];
}
return $unavailable;
}
function parseRaidStatus($output) {
$status = ['active' => false, 'status' => 'unavailable', 'type' => 'N/A', 'arrays' => []];
if (!is_string($output) || trim($output) === '') return $status;
preg_match_all('/^(md\S+)\s*:\s*(active|inactive)\s+([^\r\n]+)((?:\r?\n[ \t]+[^\r\n]*)*)/m', $output, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$details = $match[3] . $match[4];
preg_match('/\b(raid\d+|linear|multipath)\b/', $match[3], $level);
$state = $match[2] === 'active' ? 'unknown' : 'inactive';
if ($state !== 'inactive') {
if (preg_match('/\[([U_]+)\]/', $details, $members)) {
$state = strpos($members[1], '_') !== false ? 'degraded' : 'healthy';
} elseif (in_array($level[1] ?? '', ['raid0', 'linear'], true)) {
$state = 'active'; // Non-redundant arrays do not expose a [UU] health bitmap.
}
if (preg_match('/\b(recovery|resync|reshape|check|repair)\s*=/', $details)) {
if ($state !== 'degraded') $state = 'syncing';
}
if (strpos($details, '(F)') !== false) $state = 'degraded';
}
$status['arrays'][] = ['name' => $match[1], 'type' => $level[1] ?? 'N/A', 'status' => $state];
}
if (!$status['arrays']) {
$status['status'] = strpos($output, 'Personalities :') !== false ? 'not configured' : 'unavailable';
return $status;
}
$states = array_column($status['arrays'], 'status');
$status['active'] = !in_array('inactive', $states, true);
$status['type'] = implode(', ', array_unique(array_column($status['arrays'], 'type')));
foreach (['inactive', 'degraded', 'unknown', 'syncing', 'active', 'healthy'] as $state) {
if (in_array($state, $states, true)) { $status['status'] = $state; break; }
}
return $status;
}
function parseSystemStats($cpuOutput, $memOutput) {
$idle = is_string($cpuOutput) ? str_replace(',', '.', trim($cpuOutput)) : '';
$cpuAvailable = is_numeric($idle) && (float) $idle >= 0 && (float) $idle <= 100;
$cpu = ['available_data' => $cpuAvailable, 'usage' => $cpuAvailable ? number_format(100 - (float) $idle, 1, '.', '') : null];
$memory = array_fill_keys(['total', 'used', 'available', 'usage', 'swap_total', 'swap_used', 'swap_free', 'swap_usage'], 'N/A');
$memory['available_data'] = false;
$parts = is_string($memOutput) ? explode(',', trim($memOutput)) : [];
if (count($parts) === 6 && count(array_filter($parts, 'is_numeric')) === 6 && min(array_map('floatval', $parts)) >= 0 && (float) $parts[0] > 0) {
[$total, $used, $available, $swapTotal, $swapUsed, $swapFree] = array_map('floatval', $parts);
$memory = [
'available_data' => true,
'total' => formatBytes($total * 1024 * 1024),
'used' => formatBytes($used * 1024 * 1024),
'available' => formatBytes($available * 1024 * 1024),
'usage' => min(100, round($used / $total * 100, 1)) . '%',
'swap_total' => formatBytes($swapTotal * 1024 * 1024),
'swap_used' => formatBytes($swapUsed * 1024 * 1024),
'swap_free' => formatBytes($swapFree * 1024 * 1024),
'swap_usage' => ($swapTotal > 0 ? min(100, round($swapUsed / $swapTotal * 100, 1)) : 0) . '%',
];
}
return ['cpu' => $cpu, 'memory' => $memory];
}
function formatBytes($bytes) {
$units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
$bytes = max((float) $bytes, 0);
$power = min($bytes > 0 ? (int) floor(log($bytes, 1024)) : 0, count($units) - 1);
return round($bytes / pow(1024, $power), 2) . ' ' . $units[$power];
}
+30 -39
View File
@@ -1,21 +1,12 @@
<?php
// Include this at the top to see potential errors
// Comment out in production
ini_set('display_errors', 1);
error_reporting(E_ALL);
session_start();
if(!isset($_SESSION['uname'])){
header("location: ../../index.php");
session_destroy();
exit;
}
require_once __DIR__ . '/../../includes/auth.php';
requireLogin('../../index.php');
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../includes/workspace.php';
$sftp = initializeSFTP();
$filePath = isset($_GET['file']) ? $_GET['file'] : '';
$filePath = requireRemotePath($_GET['file'] ?? $defaultPath, $defaultPath);
$content = '';
$fileName = basename($filePath);
@@ -28,7 +19,7 @@ if (!empty($filePath) && $sftp->file_exists($filePath) && !$sftp->is_dir($filePa
if (in_array(strtolower($extension), $editableExtensions)) {
$editable = true;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['content']) && isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true) {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['content']) && hasPermission('upPer')) {
$newContent = $_POST['content'];
if ($sftp->put($filePath, $newContent)) {
@@ -51,34 +42,33 @@ if (!empty($filePath) && $sftp->file_exists($filePath) && !$sftp->is_dir($filePa
$showLineNumbers = in_array(strtolower($extension), ['php', 'js', 'html', 'css', 'py', 'java', 'c', 'cpp', 'h', 'hpp', 'rb', 'sh', 'xml', 'json']);
$parentDir = dirname($filePath);
$parentPath = remoteDirname($filePath);
?>
<!DOCTYPE html>
<html lang="en" data-bs-theme="<?= isset($_COOKIE['theme']) ? $_COOKIE['theme'] : 'light' ?>">
<html lang="en">
<head>
<title>Edit File - <?= htmlspecialchars($fileName) ?></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../../css/bootstrap.css">
<link rel="stylesheet" href="../../css/modern.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="css/open.css">
<script src="../../js/bootstrap.bundle.js"></script>
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/modern.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/workspace.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'content/ftp/css/open.css') ?>">
<script src="<?= workspaceAsset('../../', 'js/theme.js') ?>"></script>
<script src="<?= workspaceAsset('../../', 'js/ui.js') ?>" defer></script>
</head>
<body>
<div class="d-flex justify-content-end p-3">
<button id="themeToggle" class="btn btn-sm theme-toggle">
<i class="bi"></i>
<span id="themeText"></span>
</button>
</div>
<div class="custom-container dark-box">
<div class="header-container">
<h1>Edit File: <?= htmlspecialchars($fileName) ?></h1>
<a href="index.php?path=<?= urlencode($parentDir) ?>" class="btn btn-secondary">Back to File List</a>
</div>
<?php renderWorkspaceStart([
'root' => '../../',
'active' => 'files',
'section' => 'File editor',
'eyebrow' => 'File editor',
'title' => $fileName,
'description' => 'View and edit text files in your remote workspace.',
'connected' => true,
'back' => 'index.php?path=' . urlencode($parentPath),
'class' => 'editor-page',
]); ?>
<section class="content-panel">
<?php if (isset($error)): ?>
<div class="alert alert-danger"><?= $error ?></div>
<?php elseif ($editable): ?>
@@ -94,7 +84,7 @@ $parentDir = dirname($filePath);
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<label for="editor" class="form-label">File Content</label>
<?php if (!isset($_SESSION["upPer"]) || $_SESSION["upPer"] != true): ?>
<?php if (!hasPermission('upPer')): ?>
<span class="readonly-notice">Read-only mode (you don't have upload permissions)</span>
<?php endif; ?>
</div>
@@ -102,19 +92,20 @@ $parentDir = dirname($filePath);
<?php if ($showLineNumbers): ?>
<div id="lineNumbers" class="line-numbers"></div>
<?php endif; ?>
<textarea id="editor" name="content" class="form-control" <?= (!isset($_SESSION["upPer"]) || $_SESSION["upPer"] != true) ? 'readonly' : '' ?>><?= htmlspecialchars($content) ?></textarea>
<textarea id="editor" name="content" class="form-control" <?= (!hasPermission('upPer')) ? 'readonly' : '' ?>><?= htmlspecialchars($content) ?></textarea>
</div>
</div>
<?php if (isset($_SESSION["upPer"]) && $_SESSION["upPer"] == true): ?>
<?php if (hasPermission('upPer')): ?>
<div class="mb-3">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
<?php endif; ?>
</form>
<?php endif; ?>
</div>
<script src="../../js/theme.js"></script>
</section>
<?php renderWorkspaceEnd(); ?>
<script>
function updateLineNumbers() {
const editor = document.getElementById('editor');
+54 -69
View File
@@ -1,20 +1,16 @@
<?php
session_start();
if(!isset($_SESSION['uname'])){
header("location: ../../index.php");
session_destroy();
exit;
}
require_once __DIR__ . '/../../includes/auth.php';
requireLogin('../../index.php');
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../includes/workspace.php';
$sftp = initializeSFTP();
if (!isset($_GET['file'])) {
die("No file specified");
}
$filePath = $_GET['file'];
$filePath = requireRemotePath($_GET['file'], $defaultPath);
$fileName = basename($filePath);
$fileSize = $sftp->stat($filePath)['size'];
@@ -104,42 +100,40 @@ if (isset($_GET['stream'])) {
break;
}
}
} catch (Exception $e) {
error_log("Streaming error: " . $e->getMessage());
} catch (Exception $exception) {
error_log("Streaming error: " . $exception->getMessage());
}
exit;
}
?>
<!DOCTYPE html>
<html lang="en" data-bs-theme="<?= isset($_COOKIE['theme']) ? $_COOKIE['theme'] : 'light' ?>">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF Viewer</title>
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="../../css/bootstrap.css">
<link rel="stylesheet" href="../../css/modern.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="css/pdf.css">
<script src="../../js/bootstrap.bundle.js"></script>
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/modern.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/workspace.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'content/ftp/css/pdf.css') ?>">
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.js"></script>
<script src="<?= workspaceAsset('../../', 'js/theme.js') ?>"></script>
<script src="<?= workspaceAsset('../../', 'js/ui.js') ?>" defer></script>
</head>
<body>
<div class="d-flex justify-content-end p-3">
<button id="themeToggle" class="btn btn-sm theme-toggle">
<i class="bi"></i>
<span id="themeText"></span>
</button>
</div>
<div class="custom-container dark-box">
<header class="text-center border-bottom m-5">
<h1 class="mb-4">PDF Viewer</h1>
<div class="mb-3">
<a href="index.php?path=<?= urlencode(dirname($filePath)) ?>" class="btn btn-primary">Back to Files</a>
</div>
</header>
<?php renderWorkspaceStart([
'root' => '../../',
'active' => 'files',
'section' => 'PDF viewer',
'eyebrow' => 'Document preview',
'title' => $fileName,
'description' => 'Browse your document without leaving the workspace.',
'connected' => true,
'back' => 'index.php?path=' . urlencode(remoteDirname($filePath)),
'class' => 'pdf-page',
]); ?>
<section class="content-panel pdf-workspace" aria-label="PDF document">
<div id="pdfContainer" class="container-fluid position-relative">
<div id="loadingMessage" class="d-flex align-items-center justify-content-center w-100 h-100">
<div class="bg-dark bg-opacity-75 text-white p-3 rounded">
@@ -151,46 +145,34 @@ if (isset($_GET['stream'])) {
<div class="controls">
<div class="btn-group" role="group" aria-label="PDF Navigation">
<button id="prev" class="btn btn-light border">
<i class="bi bi-chevron-left"></i> Previous
<button id="prev" class="btn btn-light border" disabled>
<i class="ui-icon icon-chevron-left"></i> Previous
</button>
<button class="btn btn-light border disabled">
<span class="btn btn-light border" role="status">
Page <span id="pageNum"></span> of <span id="pageCount"></span>
</span>
<button id="next" class="btn btn-light border" disabled>
Next <i class="ui-icon icon-chevron-right"></i>
</button>
<button id="next" class="btn btn-light border">
Next <i class="bi bi-chevron-right"></i>
</button>
<button id="fullscreen" class="btn btn-light border">
<i class="bi bi-fullscreen"></i>
<button id="fullscreen" aria-label="Toggle fullscreen" class="btn btn-light border">
<i class="ui-icon icon-fullscreen"></i>
</button>
</div>
</div>
<div class="actionButton">
<?php if (isset($_SESSION["downPer"]) && $_SESSION["downPer"] == true) : ?>
<?php if (hasPermission('downPer')) : ?>
<a href="download.php?file=<?= urlencode($filePath) ?>" class="btn btn-success m-3">Download</a>
<?php endif; ?>
</div>
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3 m-3">
<span class="text-muted">Developed by Michal Sedlák</span>
<div class="d-flex gap-3">
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
<img src="../../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
</a>
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
<img src="../../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
</a>
</div>
</footer>
</div>
</section>
<?php renderWorkspaceEnd(); ?>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
const url = 'pdf.php?file=<?= urlencode($filePath) ?>&stream=1';
const pdfFileName = '<?= htmlspecialchars($fileName) ?>';
const pdfFileName = <?= json_encode($fileName, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
let currentPage = 1;
let pdfDoc = null;
@@ -215,13 +197,14 @@ if (isset($_GET['stream'])) {
currentPage = Math.min(PdfPageStorage.loadPage(), pdfDoc.numPages);
renderPage(currentPage);
document.getElementById('loadingMessage').classList.add('d-none');
await renderPage(currentPage);
} catch (error) {
console.error('Error loading PDF:', error);
const loadingMessage = document.getElementById('loadingMessage');
loadingMessage.querySelector('div').classList.add('bg-danger');
loadingMessage.querySelector('div').innerHTML = '<i class="bi bi-exclamation-triangle"></i> Error loading PDF';
if (loadingMessage) {
loadingMessage.querySelector('div').className = 'alert alert-danger';
loadingMessage.querySelector('div').textContent = 'Unable to load this PDF.';
}
}
}
@@ -230,7 +213,7 @@ if (isset($_GET['stream'])) {
const pageContainer = document.createElement('div');
pageContainer.className = 'page-container';
const windowWidth = window.innerWidth;
const windowWidth = document.getElementById('pdfContainer').clientWidth - 32;
const windowHeight = window.innerHeight;
const viewport = page.getViewport({ scale: 1.0 });
@@ -262,6 +245,8 @@ if (isset($_GET['stream'])) {
document.getElementById('pdfContainer').innerHTML = '';
document.getElementById('pdfContainer').appendChild(pageContainer);
document.getElementById('pageNum').textContent = pageNumber;
document.getElementById('prev').disabled = pageNumber <= 1;
document.getElementById('next').disabled = pageNumber >= pdfDoc.numPages;
await page.render({
canvasContext: context,
@@ -278,7 +263,7 @@ if (isset($_GET['stream'])) {
});
document.getElementById('next').addEventListener('click', () => {
if (currentPage < pdfDoc.numPages) {
if (pdfDoc && currentPage < pdfDoc.numPages) {
currentPage++;
renderPage(currentPage);
}
@@ -290,14 +275,14 @@ if (isset($_GET['stream'])) {
} else if (e.key === 'ArrowLeft' && currentPage > 1) {
currentPage--;
renderPage(currentPage);
} else if (e.key === 'ArrowRight' && currentPage < pdfDoc.numPages) {
} else if (e.key === 'ArrowRight' && pdfDoc && currentPage < pdfDoc.numPages) {
currentPage++;
renderPage(currentPage);
}
});
window.addEventListener('resize', () => {
if (currentPage) {
if (pdfDoc) {
renderPage(currentPage);
}
});
@@ -305,28 +290,28 @@ if (isset($_GET['stream'])) {
let isFullscreen = false;
document.getElementById('fullscreen').addEventListener('click', () => {
const container = document.querySelector('.custom-container');
const container = document.querySelector('.pdf-workspace');
const fullscreenBtn = document.getElementById('fullscreen');
const fullscreenIcon = fullscreenBtn.querySelector('i');
if (!isFullscreen) {
container.classList.add('fullscreen-mode');
document.body.style.overflow = 'hidden';
fullscreenIcon.classList.remove('bi-fullscreen');
fullscreenIcon.classList.add('bi-fullscreen-exit');
fullscreenIcon.classList.remove('icon-fullscreen');
fullscreenIcon.classList.add('icon-fullscreen-exit');
} else {
container.classList.remove('fullscreen-mode');
document.body.style.overflow = '';
fullscreenIcon.classList.remove('bi-fullscreen-exit');
fullscreenIcon.classList.add('bi-fullscreen');
fullscreenIcon.classList.remove('icon-fullscreen-exit');
fullscreenIcon.classList.add('icon-fullscreen');
}
isFullscreen = !isFullscreen;
renderPage(currentPage);
if (pdfDoc) renderPage(currentPage);
});
loadPDF();
</script>
<script src="../../js/theme.js"></script>
</body>
</html>
+11 -19
View File
@@ -1,18 +1,7 @@
<?php
session_start();
if(!isset($_SESSION['uname'])){
header("location: ../../index.php");
session_destroy();
exit;
}
if(!isset($_SESSION["upPer"]) || $_SESSION["upPer"] != true) {
die("You don't have permission to rename files or directories.");
}
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../includes/auth.php';
requireLogin('../../index.php');
requirePermission('upPer', 'You do not have permission to perform this operation.');
require_once __DIR__ . '/../../config/config.php';
$sftp = initializeSFTP();
@@ -20,23 +9,26 @@ $sftp = initializeSFTP();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$oldPath = isset($_POST['path']) ? $_POST['path'] : '';
$newName = isset($_POST['newName']) ? trim($_POST['newName']) : '';
$isDirectory = isset($_POST['isDirectory']) && $_POST['isDirectory'] == '1';
if (empty($oldPath) || empty($newName)) {
die("Missing required information for renaming.");
}
$dirPath = dirname($oldPath);
$oldName = basename($oldPath);
$oldPath = requireRemotePath($oldPath, $defaultPath);
if ($oldPath === $defaultPath || !isRemoteNameValid($newName)) {
http_response_code(400);
exit('Invalid item name or workspace root.');
}
$directoryPath = remoteDirname($oldPath);
$newPath = $dirPath . '/' . $newName;
$newPath = rtrim($directoryPath, '/') . '/' . $newName;
if ($sftp->file_exists($newPath) || $sftp->is_dir($newPath)) {
die("Error: A file or directory with this name already exists.");
}
if ($sftp->rename($oldPath, $newPath)) {
header("Location: index.php?path=" . urlencode($dirPath));
header("Location: index.php?path=" . urlencode($directoryPath));
exit;
} else {
die("Failed to rename the item. Please try again.");
+99 -382
View File
@@ -1,58 +1,23 @@
<?php
// Include this at the top to see potential errors
// Comment out in production
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../includes/auth.php';
requireLogin('../../index.php');
session_start();
requirePermission('admin', 'You do not have permission to view server statistics.');
if (empty($_SESSION['admin']) || $_SESSION['admin'] !== true) {
session_destroy();
header("location: ../../index.php");
exit();
}
require_once __DIR__ . '/../config/config.php';
use phpseclib3\Net\SSH2;
// $ssh_host = 'localhost';
// $ssh_username = 'uname';
// $ssh_password = 'pswd';
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../includes/workspace.php';
// The configuration helper authenticates using SSH_HOST, SSH_USER and SSH_PASS.
$ssh = getSSHConnection();
if (!$ssh->login($ssh_username, $ssh_password)) {
die('SSH login failed');
}
function getDiskData($ssh, $device) {
return $ssh->exec("df -h $device");
}
require_once __DIR__ . '/lib/serverstats.php';
function getSystemData($ssh) {
// Get memory info using awk to clean up the output
$meminfo = $ssh->exec("free -m | awk 'NR==2{printf \"%s,%s,%s,\", $2,$3,$7} NR==3{printf \"%s,%s,%s\", $2,$3,$4}'");
// Use vmstat to get CPU idle percentage, more reliable and commonly available
$cpuIdle = $ssh->exec("vmstat 1 2 | tail -1 | awk '{print $15}'");
return [
'storage' => [
'system' => parseDiskData($ssh->exec('df -h /dev/ubuntu-vg/ubuntu-lv')),
'data' => parseDiskData($ssh->exec('df -h /dev/sdb1'))
],
'raid' => parseRaidStatus($ssh->exec('cat /proc/mdstat')),
'resources' => parseSystemStats($cpuIdle, $meminfo),
'lastUpdate' => date('Y-m-d H:i:s')
];
}
$systemData = getSystemData($ssh);
$systemData = getSystemData($ssh, $defaultPath);
$monitors = [
[
'id' => 'SystemDisk',
'title' => 'System Disk (sda)',
'title' => 'System filesystem',
'icon' => 'hdd',
'color' => 'primary',
'type' => 'storage',
@@ -60,7 +25,7 @@ $monitors = [
],
[
'id' => 'DataDisk',
'title' => 'Data Disk (sdb)',
'title' => 'Storage filesystem',
'icon' => 'hdd',
'color' => 'primary',
'type' => 'storage',
@@ -91,349 +56,100 @@ $monitors = [
]
];
function parseDiskData($output) {
$lines = explode("\n", trim($output));
if (count($lines) < 2) return null;
$values = preg_split('/\s+/', trim($lines[1]));
return [
'size' => $values[1] ?? 'N/A',
'used' => $values[2] ?? 'N/A',
'available' => $values[3] ?? 'N/A',
'usage' => $values[4] ?? '0%'
];
}
function parseRaidStatus($output) {
if (empty($output)) {
return [
'active' => false,
'status' => 'unknown',
'type' => 'N/A'
];
}
$status = [
'active' => false,
'status' => 'unknown',
'type' => 'N/A'
];
$lines = explode("\n", trim($output));
foreach ($lines as $line) {
if (preg_match('/active\s+(\w+)/', $line, $matches)) {
$status['active'] = true;
$status['type'] = $matches[1] ?? 'N/A';
}
if (strpos($line, '[UU]') !== false) {
$status['status'] = 'healthy';
} elseif (strpos($line, '_') !== false) {
$status['status'] = 'degraded';
}
}
return $status;
}
function parseSystemStats($cpuOutput, $memOutput) {
// Parse CPU stats - convert idle percentage to usage percentage
$idlePercent = floatval(trim(str_replace(',', '.', $cpuOutput)));
$cpuUsage = 100 - $idlePercent; // Convert idle to usage percentage
if (is_nan($cpuUsage) || $cpuUsage < 0 || $cpuUsage > 100) {
$cpuUsage = 0; // Default to 0% if parsing fails
}
$cpu = ['usage' => number_format($cpuUsage, 1)];
// Parse memory stats
$parts = explode(',', $memOutput);
if (count($parts) >= 6) {
$memTotal = intval($parts[0]);
$memUsed = intval($parts[1]);
$memAvail = intval($parts[2]);
$swapTotal = intval($parts[3]);
$swapUsed = intval($parts[4]);
$swapFree = intval($parts[5]);
$memory = [
'total' => number_format($memTotal / 1024, 2) . 'G',
'used' => number_format($memUsed / 1024, 2) . 'G',
'available' => number_format($memAvail / 1024, 2) . 'G',
'usage' => ($memTotal > 0 ? round(($memUsed / $memTotal) * 100, 1) : 0) . '%',
'swap_total' => number_format($swapTotal / 1024, 2) . 'G',
'swap_used' => number_format($swapUsed / 1024, 2) . 'G',
'swap_free' => number_format($swapFree / 1024, 2) . 'G',
'swap_usage' => ($swapTotal > 0 ? round(($swapUsed / $swapTotal) * 100, 1) : 0) . '%'
];
} else {
$memory = [
'total' => '0G',
'used' => '0G',
'available' => '0G',
'usage' => '0%',
'swap_total' => '0G',
'swap_used' => '0G',
'swap_free' => '0G',
'swap_usage' => '0%'
];
}
return ['cpu' => $cpu, 'memory' => $memory];
}
function convertToBytes($size) {
if (preg_match('/^([\d.]+)([KMGT]?)i?B?$/', trim($size), $matches)) {
$value = floatval($matches[1]);
$unit = strtoupper($matches[2]);
switch ($unit) {
case 'P': $value *= 1024;
case 'T': $value *= 1024;
case 'G': $value *= 1024;
case 'M': $value *= 1024;
case 'K': $value *= 1024;
}
return $value;
}
return 0;
}
function formatBytes($bytes, $forceUnit = '') {
$units = ['B', 'K', 'M', 'G', 'T', 'P'];
$bytes = max($bytes, 0);
if ($forceUnit) {
$unitIndex = array_search($forceUnit, $units);
if ($unitIndex !== false) {
$bytes /= pow(1024, $unitIndex);
return round($bytes, 2) . $forceUnit;
}
}
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . $units[$pow];
}
?>
<!DOCTYPE html>
<html lang="en" data-bs-theme="<?= isset($_COOKIE['theme']) ? $_COOKIE['theme'] : 'light' ?>">
<html lang="en">
<head>
<title>Server Status</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../../css/bootstrap.css">
<link rel="stylesheet" href="../../css/modern.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="css/serverstat.css">
<script src="../../js/bootstrap.bundle.js"></script>
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/modern.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/workspace.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'content/ftp/css/serverstat.css') ?>">
<script src="<?= workspaceAsset('../../', 'js/theme.js') ?>"></script>
<script src="<?= workspaceAsset('../../', 'js/ui.js') ?>" defer></script>
</head>
<body class="container-fluid text-center">
<div class="d-flex justify-content-end p-3">
<button id="themeToggle" class="btn btn-sm theme-toggle">
<i class="bi"></i>
<span id="themeText"></span>
</button>
</div>
<?php renderWorkspaceStart([
'root' => '../../',
'active' => 'server',
'section' => 'Server status',
'eyebrow' => 'Server overview',
'title' => 'Server status',
'description' => 'Keep an eye on your storage, RAID health and system resources.',
'connected' => true,
'class' => 'server-page',
]); ?>
<div class="custom-container dark-box">
<header class="row border-bottom m-5">
<h1>Server Status</h1>
<div class="mb-3 p-3">
<a href="../logout.php" class="btn btn-danger">Logout</a>
<a href="../changepassword.php" class="btn btn-warning">Change Password</a>
<a href="../adminpanel.php" class="btn btn-primary">Admin Panel</a>
<a href="index.php" class="btn btn-primary">SFTP</a>
</div>
</header>
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="card shadow-sm bg-body-tertiary">
<div class="card-header monitor-header">
<div class="d-flex justify-content-between align-items-center">
<h1 class="h4 mb-0">
<i class="bi bi-server text-primary me-2"></i>
Server Disk Status
</h1>
<span class="badge bg-light text-dark">
<i class="bi bi-clock me-1"></i>
<?= htmlspecialchars($systemData['lastUpdate']) ?>
</span>
</div>
</div>
<div class="card-body">
<div class="row g-4">
<?php foreach ($monitors as $monitor): ?>
<div class="col-md-6">
<div class="monitor-card card h-100 border-<?= $monitor['color'] ?>">
<div class="card-header bg-<?= $monitor['color'] ?> bg-opacity-10">
<h3 class="h5 mb-0">
<i class="bi bi-<?= $monitor['icon'] ?> text-<?= $monitor['color'] ?> me-2"></i>
<?= $monitor['title'] ?>
</h3>
</div>
<div class="card-body">
<?php if ($monitor['type'] === 'storage'):
$diskData = $systemData['storage'][$monitor['source']];
$usagePercent = intval(rtrim($diskData['usage'], '%'));
?>
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<span>Storage Usage</span>
<span><?= $diskData['usage'] ?></span>
</div>
<div class="progress" style="height: 10px">
<div class="progress-bar bg-<?= $usagePercent > 90 ? 'danger' : ($usagePercent > 75 ? 'warning' : 'success') ?>"
role="progressbar"
style="width: <?= $usagePercent ?>%"
aria-valuenow="<?= $usagePercent ?>"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
</div>
<div class="row text-center g-2">
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">Total</div>
<div class="fw-bold"><?= $diskData['size'] ?></div>
</div>
</div>
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">Used</div>
<div class="fw-bold"><?= $diskData['used'] ?></div>
</div>
</div>
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">Free</div>
<div class="fw-bold"><?= $diskData['available'] ?></div>
</div>
</div>
</div>
<?php elseif ($monitor['type'] === 'raid'): ?>
<div class="text-center">
<div class="display-4 mb-2">
<i class="bi bi-<?= $systemData['raid']['active'] ? 'check-circle-fill text-success' : 'x-circle-fill text-danger' ?>"></i>
</div>
<h4 class="h6"><?= $systemData['raid']['type'] ?> Array</h4>
<span class="badge bg-<?= $systemData['raid']['status'] === 'healthy' ? 'success' : 'warning' ?>">
<?= ucfirst($systemData['raid']['status']) ?>
</span>
</div>
<?php elseif ($monitor['type'] === 'resources' && $monitor['source'] === 'cpu'): ?>
<div class="cpu-gauge">
<canvas id="cpuGauge" width="150" height="150" data-value="<?= $systemData['resources']['cpu']['usage'] ?>"></canvas>
</div>
<?php elseif ($monitor['type'] === 'resources' && $monitor['source'] === 'memory'):
$memData = $systemData['resources']['memory'];
$usagePercent = floatval(rtrim($memData['usage'], '%'));
$swapPercent = floatval(rtrim($memData['swap_usage'], '%'));
?>
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<span>Memory Usage</span>
<span><?= $memData['usage'] ?></span>
</div>
<div class="progress" style="height: 10px">
<div class="progress-bar bg-<?= $usagePercent > 90 ? 'danger' : ($usagePercent > 75 ? 'warning' : 'success') ?>"
role="progressbar"
style="width: <?= $usagePercent ?>%"
aria-valuenow="<?= $usagePercent ?>"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
</div>
<div class="row text-center g-2 mb-3">
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">Total</div>
<div class="fw-bold"><?= $memData['total'] ?></div>
</div>
</div>
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">Used</div>
<div class="fw-bold"><?= $memData['used'] ?></div>
</div>
</div>
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">Available</div>
<div class="fw-bold"><?= $memData['available'] ?></div>
</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<span>Swap Usage</span>
<span><?= $memData['swap_usage'] ?></span>
</div>
<div class="progress" style="height: 10px">
<div class="progress-bar bg-<?= $swapPercent > 90 ? 'danger' : ($swapPercent > 75 ? 'warning' : 'success') ?>"
role="progressbar"
style="width: <?= $swapPercent ?>%"
aria-valuenow="<?= $swapPercent ?>"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
</div>
<div class="row text-center g-2">
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">Total</div>
<div class="fw-bold"><?= $memData['swap_total'] ?></div>
</div>
</div>
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">Used</div>
<div class="fw-bold"><?= $memData['swap_used'] ?></div>
</div>
</div>
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">Free</div>
<div class="fw-bold"><?= $memData['swap_free'] ?></div>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="content-toolbar"><p>Live statistics · refreshes every 5 seconds</p><span class="badge bg-light text-dark"><i class="ui-icon icon-clock" aria-hidden="true"></i> <?= htmlspecialchars($systemData['lastUpdate']) ?></span></div>
<div class="stats-grid">
<?php foreach ($monitors as $monitor): ?>
<section class="stats-cell" aria-label="<?= htmlspecialchars($monitor['title']) ?>">
<div class="monitor-card card h-100">
<div class="card-header"><h3 class="mb-0"><i class="ui-icon icon-<?= $monitor['icon'] ?> text-primary me-2" aria-hidden="true"></i><?= htmlspecialchars($monitor['title']) ?></h3></div>
<div class="card-body">
<?php if ($monitor['type'] === 'storage'):
$diskData = $systemData['storage'][$monitor['source']];
?>
<?php if (!$diskData['available_data']): ?>
<p class="text-muted" role="status">Storage statistics are unavailable for this path.</p>
<?php else:
$usagePercent = min(100, (int) rtrim($diskData['usage'], '%'));
?>
<p class="small text-muted"><?= htmlspecialchars($diskData['filesystem']) ?> · <?= htmlspecialchars($monitor['source'] === 'system' ? '/' : $defaultPath) ?></p>
<div class="d-flex justify-content-between"><span>Storage usage</span><strong><?= htmlspecialchars($diskData['usage']) ?></strong></div>
<div class="progress mb-4"><div class="progress-bar bg-<?= $usagePercent > 90 ? 'danger' : ($usagePercent > 75 ? 'warning' : 'success') ?>" role="progressbar" style="width:<?= $usagePercent ?>%" aria-label="Storage usage" aria-valuenow="<?= $usagePercent ?>" aria-valuemin="0" aria-valuemax="100"></div></div>
<div class="row text-center">
<?php foreach (['size' => 'Total', 'used' => 'Used', 'available' => 'Free'] as $key => $label): ?>
<div class="col-4"><div class="p-2 border rounded"><div class="small text-muted"><?= $label ?></div><div class="fw-bold"><?= htmlspecialchars($diskData[$key]) ?></div></div></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php elseif ($monitor['type'] === 'raid'): ?>
<div class="raid-summary">
<i class="ui-icon icon-<?= $systemData['raid']['status'] === 'healthy' ? 'check-circle-fill text-success' : 'x-circle-fill text-warning' ?>" aria-hidden="true"></i>
<h4><?= $systemData['raid']['arrays'] ? htmlspecialchars($systemData['raid']['type']) . ' Array' : 'Linux MD RAID' ?></h4>
<span class="badge"><?= htmlspecialchars(ucfirst($systemData['raid']['status'])) ?></span>
<?php foreach ($systemData['raid']['arrays'] as $array): ?>
<p class="small text-muted mt-2"><?= htmlspecialchars($array['name'] . ' · ' . $array['type'] . ' · ' . $array['status']) ?></p>
<?php endforeach; ?>
</div>
</div>
<?php elseif ($monitor['source'] === 'cpu'): ?>
<?php if (!$systemData['resources']['cpu']['available_data']): ?>
<p class="text-muted" role="status">CPU statistics are unavailable.</p>
<?php else: ?>
<div class="cpu-gauge"><canvas id="cpuGauge" width="170" height="170" data-value="<?= $systemData['resources']['cpu']['usage'] ?>" aria-label="CPU usage <?= $systemData['resources']['cpu']['usage'] ?> percent"></canvas></div>
<?php endif; ?>
<?php else:
$memData = $systemData['resources']['memory'];
?>
<?php if (!$memData['available_data']): ?>
<p class="text-muted" role="status">Memory statistics are unavailable.</p>
<?php else: ?>
<?php foreach (['usage' => 'Memory usage', 'swap_usage' => 'Swap usage'] as $usageKey => $label):
$usagePercent = (float) rtrim($memData[$usageKey], '%');
$fields = $usageKey === 'usage' ? ['total' => 'Total', 'used' => 'Used', 'available' => 'Available'] : ['swap_total' => 'Total', 'swap_used' => 'Used', 'swap_free' => 'Free'];
?>
<div class="resource-section">
<div class="d-flex justify-content-between"><span><?= $label ?></span><strong><?= htmlspecialchars($memData[$usageKey]) ?></strong></div>
<div class="progress mb-3"><div class="progress-bar bg-<?= $usagePercent > 90 ? 'danger' : ($usagePercent > 75 ? 'warning' : 'success') ?>" role="progressbar" style="width:<?= $usagePercent ?>%" aria-label="<?= $label ?>" aria-valuenow="<?= $usagePercent ?>" aria-valuemin="0" aria-valuemax="100"></div></div>
<div class="row text-center">
<?php foreach ($fields as $key => $fieldLabel): ?>
<div class="col-4"><div class="p-2 border rounded"><div class="small text-muted"><?= $fieldLabel ?></div><div class="fw-bold"><?= htmlspecialchars($memData[$key]) ?></div></div></div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</div>
</section>
<?php endforeach; ?>
</div>
<?php renderWorkspaceEnd(); ?>
</div>
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3">
<span class="text-muted">Developed by Michal Sedlák</span>
<div class="d-flex gap-3">
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
<img src="../../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
</a>
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
<img src="../../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
</a>
</div>
</footer>
<script src="../../js/theme.js"></script>
<script>
let refreshInterval;
let isRefreshing = false;
@@ -463,11 +179,6 @@ function updateData(html) {
currentLastUpdate.innerHTML = newLastUpdate;
}
// Update CPU gauge if needed
const cpuValue = document.querySelector('.cpu-gauge .h4')?.textContent;
if (cpuValue) {
drawCpuGauge(parseFloat(cpuValue));
}
}
function startRefresh() {
@@ -476,7 +187,10 @@ function startRefresh() {
if (!isRefreshing) {
isRefreshing = true;
fetch(window.location.href)
.then(response => response.text())
.then(response => {
if (!response.ok || response.redirected) throw new Error('Status refresh failed');
return response.text();
})
.then(html => {
updateData(html);
isRefreshing = false;
@@ -506,8 +220,7 @@ document.addEventListener('visibilitychange', () => {
});
// Initial setup
startRefresh();
drawCpuGauge(parseFloat(document.querySelector('.cpu-gauge .h4')?.textContent || '0'));
if (!document.hidden) startRefresh();
function drawCpuGauge(value) {
const canvas = document.getElementById('cpuGauge');
@@ -527,7 +240,7 @@ function drawCpuGauge(value) {
// Draw background circle
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.strokeStyle = getComputedStyle(document.body).getPropertyValue('--bs-border-color');
ctx.strokeStyle = getComputedStyle(document.body).getPropertyValue('--line');
ctx.lineWidth = 10;
ctx.stroke();
@@ -543,7 +256,7 @@ function drawCpuGauge(value) {
// Draw percentage text inside the circle
ctx.font = 'bold 20px Arial';
ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--bs-body-color');
ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--text');
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${value.toFixed(1)}%`, centerX, centerY - 10);
@@ -558,6 +271,10 @@ window.addEventListener('load', function() {
const cpuValue = parseFloat(document.querySelector('.cpu-gauge canvas')?.getAttribute('data-value') || '0');
drawCpuGauge(cpuValue);
});
new MutationObserver(() => {
const cpuValue = document.getElementById('cpuGauge')?.dataset.value;
if (cpuValue !== undefined) drawCpuGauge(cpuValue);
}).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
</script>
</body>
</html>
+36 -59
View File
@@ -1,25 +1,19 @@
<?php
// Include this at the top to see potential errors
// Comment out in production
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../config/config.php';
// getDatabaseConnection relocated to central config
require_once __DIR__ . '/../../includes/workspace.php';
if (!isset($_GET['token'])) {
die("Invalid request. No token provided.");
}
$token = $_GET['token'];
$conn = getDatabaseConnection();
$database = getDatabaseConnection();
// Get share information
$stmt = $conn->prepare("SELECT * FROM share WHERE token = ? AND expiration > NOW()");
$stmt->bind_param("s", $token);
$stmt->execute();
$result = $stmt->get_result();
$statement = $database->prepare("SELECT * FROM share WHERE token = ? AND expiration > NOW()");
$statement->bind_param("s", $token);
$statement->execute();
$result = $statement->get_result();
if ($result->num_rows === 0) {
die("Invalid or expired share link.");
@@ -28,7 +22,9 @@ if ($result->num_rows === 0) {
$share = $result->fetch_assoc();
$filePath = $share['file_path'];
$downloadAllowed = $share['download_allowed'] == 1;
$stmt->close();
$result->free();
$statement->close();
$database->close();
// Initialize SFTP
$sftp = initializeSFTP();
@@ -41,13 +37,13 @@ $fileName = basename($filePath);
$fileExtension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$fileSize = $sftp->stat($filePath)['size'];
$imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
$videoTypes = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv'];
$audioTypes = ['mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac'];
$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
$videoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv'];
$audioExtensions = ['mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac'];
$isImage = in_array($fileExtension, $imageTypes);
$isVideo = in_array($fileExtension, $videoTypes);
$isAudio = in_array($fileExtension, $audioTypes);
$isImage = in_array($fileExtension, $imageExtensions);
$isVideo = in_array($fileExtension, $videoExtensions);
$isAudio = in_array($fileExtension, $audioExtensions);
if (!$isImage && !$isVideo && !$isAudio) {
die("Unsupported file type");
@@ -164,8 +160,8 @@ if (isset($_GET['stream'])) {
usleep(100000);
}
} catch (Exception $e) {
error_log("Shared streaming error: " . $e->getMessage());
} catch (Exception $exception) {
error_log("Shared streaming error: " . $exception->getMessage());
}
exit;
}
@@ -218,35 +214,31 @@ function formatBytes($bytes, $precision = 2) {
?>
<!DOCTYPE html>
<html lang="en" data-bs-theme="<?= isset($_COOKIE['theme']) ? $_COOKIE['theme'] : 'light' ?>">
<html lang="en">
<head>
<title>Shared Media - <?= htmlspecialchars($fileName) ?></title>
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="../../css/bootstrap.css">
<link rel="stylesheet" href="../../css/modern.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="css/view.css">
<script src="../../js/bootstrap.bundle.js"></script>
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/modern.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/workspace.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'content/ftp/css/view.css') ?>">
<script src="<?= workspaceAsset('../../', 'js/theme.js') ?>"></script>
<script src="<?= workspaceAsset('../../', 'js/ui.js') ?>" defer></script>
</head>
<body class="text-center">
<div class="d-flex justify-content-end p-3">
<button id="themeToggle" class="btn btn-sm theme-toggle">
<i class="bi"></i>
<span id="themeText"></span>
</button>
</div>
<div class="custom-container dark-box">
<header class="row border-bottom m-5">
<h1>Shared Media</h1>
<div class="mb-3">
<span class="badge bg-info">Shared Link</span>
<p class="text-muted small">This link will expire on <?= date('F j, Y, g:i a', strtotime($share['expiration'])) ?></p>
</div>
</header>
<?php renderWorkspaceStart([
'root' => '../../',
'active' => 'shared',
'section' => 'Shared file',
'eyebrow' => 'Shared with you',
'title' => $fileName,
'description' => 'Preview and access the file shared with you.',
'guest' => true,
'class' => 'media-page',
]); ?>
<section class="row">
<div class="content-toolbar"><span class="badge">Shared link</span><p>Available until <?= htmlspecialchars(date('F j, Y, g:i a', strtotime($share['expiration']))) ?></p></div>
<section class="content-panel media-panel">
<article class="col-12">
<div class="media-container position-relative">
<?php if ($isImage): ?>
@@ -293,22 +285,7 @@ function formatBytes($bytes, $precision = 2) {
</article>
</section>
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3 m-3">
<span class="text-muted">Developed by Michal Sedlák</span>
<div class="d-flex gap-3">
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
<img src="../../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
</a>
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
<img src="../../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
</a>
</div>
</footer>
</div>
<script src="../../js/theme.js"></script>
<?php renderWorkspaceEnd(); ?>
<script>
document.addEventListener('DOMContentLoaded', () => {
+65 -110
View File
@@ -1,130 +1,85 @@
<?php
// Include this at the top to see potential errors
// Comment out in production
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../includes/auth.php';
requireLogin('../../index.php');
requirePermission('upPer', 'You do not have permission to upload files.');
session_write_close();
require_once __DIR__ . '/../../config/config.php';
use phpseclib3\Net\SFTP;
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);
exit('No files received or invalid request.');
}
$currentPath = requireRemotePath($_POST['currentPath'] ?? $defaultPath, $defaultPath);
$sftp = initializeSFTP();
$currentPath = isset($_POST['currentPath']) ? normalizePath($_POST['currentPath']) : $defaultPath;
if (strpos($currentPath, $defaultPath) !== 0) {
$currentPath = $defaultPath;
if (isset($_POST['create_dirs'])) {
$directories = json_decode($_POST['create_dirs'], true);
if (!is_array($directories)) {
http_response_code(400);
exit('Invalid directory list.');
}
foreach ($directories as $directory) {
$directoryPath = requireRemotePath(rtrim($currentPath, '/') . '/' . $directory, $currentPath);
if (!$sftp->is_dir($directoryPath) && !$sftp->mkdir($directoryPath, 0755, true)) {
http_response_code(500);
exit('Failed to create upload directory.');
}
}
}
function createDirectoryRecursive($sftp, $path) {
if ($sftp->is_dir($path)) {
return true;
}
$parent = dirname($path);
if ($parent != '/' && !$sftp->is_dir($parent)) {
createDirectoryRecursive($sftp, $parent);
}
return $sftp->mkdir($path, 0755);
$uploadedFiles = $_FILES['files'] ?? null;
if (!$uploadedFiles || empty($uploadedFiles['name'][0])) {
http_response_code(400);
exit('No files received or file size exceeds PHP limits.');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['create_dirs'])) {
$directories = json_decode($_POST['create_dirs'], true);
$uploadErrors = [
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize directive in php.ini',
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE directive in the HTML form',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload',
];
$uploadMessages = [];
$anySuccess = false;
$failureStatus = 500;
if (is_array($directories)) {
foreach ($directories as $dir) {
$remoteDirPath = $currentPath . '/' . $dir;
createDirectoryRecursive($sftp, $remoteDirPath);
}
}
foreach ($uploadedFiles['tmp_name'] as $index => $temporaryPath) {
$fileName = $uploadedFiles['name'][$index];
$relativePath = $_POST['paths'][$index] ?? '';
$uploadError = $uploadedFiles['error'][$index];
if ($uploadError !== UPLOAD_ERR_OK) {
$failureStatus = 400;
$uploadMessages[] = $fileName . ': ' . ($uploadErrors[$uploadError] ?? 'Unknown upload error');
continue;
}
if (!isset($_FILES['files']) || empty($_FILES['files']['name'][0])) {
echo "No files received or file size exceeds PHP limits.";
$remotePath = requireRemotePath(rtrim($currentPath, '/') . '/' . ($relativePath !== '' ? $relativePath : $fileName), $currentPath);
if ($remotePath === $currentPath) {
http_response_code(400);
exit;
exit('Invalid upload path.');
}
$directoryPath = remoteDirname($remotePath);
if (!$sftp->is_dir($directoryPath) && !$sftp->mkdir($directoryPath, 0755, true)) {
$uploadMessages[] = 'Failed to create directory for: ' . $fileName;
continue;
}
if ($_FILES['files']['error'][0] !== 0) {
$error = $_FILES['files']['error'][0];
$errorMessage = "Upload error code: $error";
switch ($error) {
case UPLOAD_ERR_INI_SIZE:
$errorMessage = "File exceeds upload_max_filesize directive in php.ini";
break;
case UPLOAD_ERR_FORM_SIZE:
$errorMessage = "File exceeds MAX_FILE_SIZE directive in the HTML form";
break;
case UPLOAD_ERR_PARTIAL:
$errorMessage = "File was only partially uploaded";
break;
case UPLOAD_ERR_NO_FILE:
$errorMessage = "No file was uploaded";
break;
case UPLOAD_ERR_NO_TMP_DIR:
$errorMessage = "Missing a temporary folder";
break;
case UPLOAD_ERR_CANT_WRITE:
$errorMessage = "Failed to write file to disk";
break;
case UPLOAD_ERR_EXTENSION:
$errorMessage = "A PHP extension stopped the file upload";
break;
}
echo $errorMessage;
http_response_code(400);
exit;
}
$uploadStatus = array();
$anySuccess = false;
foreach ($_FILES['files']['tmp_name'] as $index => $tmpName) {
if (empty($tmpName)) continue;
$fileName = $_FILES['files']['name'][$index];
$relativePath = isset($_POST['paths']) && isset($_POST['paths'][$index]) ? $_POST['paths'][$index] : '';
if (!empty($relativePath)) {
$fileName = basename($relativePath);
$dirPart = dirname($relativePath);
if ($dirPart !== '.' && $dirPart !== '') {
$remoteDirPath = $currentPath . '/' . $dirPart;
if (!$sftp->is_dir($remoteDirPath)) {
createDirectoryRecursive($sftp, $remoteDirPath);
}
$remotePath = $remoteDirPath . '/' . $fileName;
} else {
$remotePath = $currentPath . '/' . $fileName;
}
} else {
$remotePath = $currentPath . '/' . $fileName;
}
if ($sftp->put($remotePath, $tmpName, SFTP::SOURCE_LOCAL_FILE)) {
$uploadStatus[] = "Uploaded: " . ($relativePath ? $relativePath : $fileName);
$anySuccess = true;
} else {
$uploadStatus[] = "Failed to upload: " . ($relativePath ? $relativePath : $fileName);
}
}
if ($anySuccess) {
echo implode("\n", $uploadStatus);
exit;
if ($sftp->put($remotePath, $temporaryPath, SFTP::SOURCE_LOCAL_FILE)) {
$uploadMessages[] = 'Uploaded: ' . ($relativePath !== '' ? $relativePath : $fileName);
$anySuccess = true;
} else {
echo "Failed to upload any files. Please check SFTP connection and permissions.";
http_response_code(500);
exit;
$uploadMessages[] = 'Failed to upload: ' . ($relativePath !== '' ? $relativePath : $fileName);
}
}
echo "No files received or invalid request.";
http_response_code(400);
exit;
if (!$anySuccess) {
http_response_code($failureStatus);
}
header('Content-Type: text/plain; charset=UTF-8');
echo implode("\n", $uploadMessages);
+52 -77
View File
@@ -1,19 +1,11 @@
<?php
session_start();
require_once __DIR__ . '/../../includes/auth.php';
requireLogin('../../index.php');
if(!isset($_SESSION['uname'])){
header("location: ../../index.php");
session_destroy();
exit;
}
// Include this at the top to see potential errors
// Comment out in production
ini_set('display_errors', 1);
error_reporting(E_ALL);
ini_set('memory_limit', '512M');
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../includes/workspace.php';
$sftp = initializeSFTP();
if (!isset($_GET['file'])) {
@@ -23,7 +15,7 @@ if (!isset($_GET['file'])) {
if (isset($_POST['createShare'])) {
$duration = intval($_POST['duration']);
$downloadAllowed = isset($_POST['downloadAllowed']) ? 1 : 0;
$filePath = $_POST['filePath'];
$filePath = requireRemotePath($_POST['filePath'], $defaultPath);
$token = bin2hex(random_bytes(16));
@@ -31,13 +23,17 @@ if (isset($_POST['createShare'])) {
$expiration = date('Y-m-d H:i:s', strtotime("+$duration hours"));
$conn = getDatabaseConnection();
$stmt = $conn->prepare("INSERT INTO share (token, file_path, expiration, download_allowed) VALUES (?, ?, ?, ?)");
$stmt->bind_param("sssi", $token, $filePath, $expiration, $downloadAllowed);
$database = getDatabaseConnection();
$statement = $database->prepare("INSERT INTO share (token, file_path, expiration, download_allowed) VALUES (?, ?, ?, ?)");
$statement->bind_param("sssi", $token, $filePath, $expiration, $downloadAllowed);
if ($stmt->execute()) {
$shareSaved = $statement->execute();
$statement->close();
$database->close();
if ($shareSaved) {
$shareUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]" .
dirname($_SERVER['PHP_SELF']) . "/shared.php?token=$token";
remoteDirname($_SERVER['PHP_SELF']) . "/shared.php?token=$token";
$_SESSION['shareCreated'] = true;
$_SESSION['shareUrl'] = $shareUrl;
@@ -46,13 +42,11 @@ if (isset($_POST['createShare'])) {
header("Location: view.php?file=" . urlencode($filePath) . "&shared=1");
exit;
} else {
$_SESSION['shareError'] = "Failed to create share: " . $conn->error;
$_SESSION['shareError'] = 'Failed to create share. Please try again.';
header("Location: view.php?file=" . urlencode($filePath) . "&shared=0");
exit;
}
$stmt->close();
$conn->close();
}
$shareCreated = false;
@@ -70,7 +64,7 @@ if (isset($_GET['shared'])) {
}
}
$filePath = $_GET['file'];
$filePath = requireRemotePath($_GET['file'], $defaultPath);
$fileName = basename($filePath);
$fileExtension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
@@ -78,13 +72,13 @@ if (!$sftp->stat($filePath)) {
die("File not found: $filePath");
}
$imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
$videoTypes = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv'];
$audioTypes = ['mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac'];
$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
$videoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv'];
$audioExtensions = ['mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac'];
$isImage = in_array($fileExtension, $imageTypes);
$isVideo = in_array($fileExtension, $videoTypes);
$isAudio = in_array($fileExtension, $audioTypes);
$isImage = in_array($fileExtension, $imageExtensions);
$isVideo = in_array($fileExtension, $videoExtensions);
$isAudio = in_array($fileExtension, $audioExtensions);
if (!$isImage && !$isVideo && !$isAudio) {
die("Unsupported file type");
@@ -237,10 +231,10 @@ if (isset($_GET['stream'])) {
// Throttle to prevent CPU overload
usleep(100000); // 100ms
}
} catch (Exception $e) {
error_log("Streaming error: " . $e->getMessage());
} catch (Exception $exception) {
error_log("Streaming error: " . $exception->getMessage());
if (isset($_GET['debug'])) {
header("X-Stream-Error: " . $e->getMessage());
header("X-Stream-Error: " . $exception->getMessage());
}
}
@@ -251,36 +245,32 @@ if (isset($_GET['stream'])) {
}
?>
<!DOCTYPE html>
<html lang="en" data-bs-theme="<?= isset($_COOKIE['theme']) ? $_COOKIE['theme'] : 'light' ?>">
<html lang="en">
<head>
<title>Media Viewer - <?= htmlspecialchars($fileName) ?></title>
<link rel="icon" href="../../img/favicon.ico" type="image/x-icon">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="../../css/bootstrap.css">
<link rel="stylesheet" href="../../css/modern.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="css/view.css">
<script src="../../js/bootstrap.bundle.js"></script>
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/modern.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'css/workspace.css') ?>">
<link rel="stylesheet" href="<?= workspaceAsset('../../', 'content/ftp/css/view.css') ?>">
<script src="<?= workspaceAsset('../../', 'js/theme.js') ?>"></script>
<script src="<?= workspaceAsset('../../', 'js/ui.js') ?>" defer></script>
</head>
<body class="text-center">
<div class="d-flex justify-content-end p-3">
<button id="themeToggle" class="btn btn-sm theme-toggle">
<i class="bi"></i>
<span id="themeText"></span>
</button>
</div>
<div class="custom-container dark-box">
<header class="row border-bottom m-5">
<h1>Media Viewer</h1>
<div class="mb-3 p-3">
<a href="index.php?path=<?= urlencode(dirname($filePath)) ?>" class="btn btn-primary">Back to Files</a>
</div>
</header>
<?php renderWorkspaceStart([
'root' => '../../',
'active' => 'files',
'section' => 'Media preview',
'eyebrow' => 'Media preview',
'title' => $fileName,
'description' => 'Preview your media, download a copy or create a shared link.',
'connected' => true,
'back' => 'index.php?path=' . urlencode(remoteDirname($filePath)),
'class' => 'media-page',
]); ?>
<section class="row">
<section class="content-panel media-panel">
<article class="col-12">
<div class="media-container position-relative">
<?php if ($isImage): ?>
@@ -302,13 +292,13 @@ if (isset($_GET['stream'])) {
</audio>
<?php endif; ?>
<?php if (isset($_SESSION["downPer"]) && $_SESSION["downPer"] == true) : ?>
<?php if (hasPermission('downPer')) : ?>
<a href="download.php?file=<?= urlencode($filePath) ?>" class="btn btn-success m-3">Download</a>
<?php endif; ?>
<!-- Add Share Button -->
<button type="button" class="btn btn-info m-3" data-bs-toggle="modal" data-bs-target="#shareModal">
<i class="bi bi-share"></i> Share
<button type="button" class="btn btn-info m-3" data-dialog="shareModal">
<i class="ui-icon icon-share"></i> Share
</button>
</div>
@@ -319,7 +309,7 @@ if (isset($_GET['stream'])) {
<div class="input-group mb-3">
<input type="text" class="form-control" id="shareUrl" value="<?= htmlspecialchars($shareUrl) ?>" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="copyShareUrl()">
<i class="bi bi-clipboard"></i> Copy
<i class="ui-icon icon-clipboard"></i> Copy
</button>
</div>
<p class="small text-muted">This link will expire on <?= date('F j, Y, g:i a', strtotime($shareExpiration)) ?></p>
@@ -356,28 +346,15 @@ if (isset($_GET['stream'])) {
</article>
</section>
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3 m-3">
<span class="text-muted">Developed by Michal Sedlák</span>
<div class="d-flex gap-3">
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../../img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
<img src="../../img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
</a>
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="../../img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
<img src="../../img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
</a>
</div>
</footer>
</div>
<?php renderWorkspaceEnd(); ?>
<!-- Share Modal -->
<div class="modal fade" id="shareModal" tabindex="-1" aria-labelledby="shareModalLabel" aria-hidden="true">
<dialog class="modal" id="shareModal" tabindex="-1" aria-labelledby="shareModalLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="shareModalLabel">Share "<?= htmlspecialchars($fileName) ?>"</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="btn-close" data-dialog-close aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="">
@@ -400,14 +377,14 @@ if (isset($_GET['stream'])) {
</div>
<div class="text-end">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-secondary" data-dialog-close>Cancel</button>
<button type="submit" name="createShare" class="btn btn-primary">Create Share Link</button>
</div>
</form>
</div>
</div>
</div>
</div>
</dialog>
<script>
function copyShareUrl() {
@@ -418,7 +395,7 @@ function copyShareUrl() {
// Show feedback
const button = shareUrl.nextElementSibling;
const originalText = button.innerHTML;
button.innerHTML = '<i class="bi bi-check"></i> Copied!';
button.innerHTML = '<i class="ui-icon icon-check"></i> Copied!';
setTimeout(() => {
button.innerHTML = originalText;
@@ -426,8 +403,6 @@ function copyShareUrl() {
}
</script>
<script src="../../js/theme.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const mediaElement = document.querySelector('video, audio');
+46 -78
View File
@@ -1,83 +1,51 @@
<?php
session_start();
if (isset($_POST['uname'], $_POST['pswd'])) {
require_once __DIR__ . '/../config/config.php';
// $username = "UNAME";
// $password = "PSWD";
// $db = "usbraidlogin";
$uname = htmlspecialchars($_POST['uname']);
$pswd = htmlspecialchars($_POST['pswd']);
$conn = getDatabaseConnection();
// charset already set in getDatabaseConnection()
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$sql = "SELECT * FROM users WHERE uname=?";
$stmt = $conn->prepare($sql);
if (!$stmt) {
$_SESSION['login_error'] = "Database error. Please try again.";
$conn->close();
header("Location: ../index.php");
exit();
}
$stmt->bind_param("s", $uname);
$stmt->execute();
$result = $stmt->get_result();
if ($result && $result->num_rows > 0) {
$login_successful = false;
while ($row = $result->fetch_assoc()) {
$hash = $row["pswd"];
$admin = (bool)$row["admin"];
$defPath = $row["defPath"];
$delPer = $row["delPer"];
$downPer = $row["downPer"];
$upPer = $row["upPer"];
if (password_verify($pswd, $hash)) {
// Login successful - store info in session
$_SESSION['uname'] = $uname;
$_SESSION['admin'] = $admin;
$_SESSION['defPath'] = $defPath;
$_SESSION['downPer'] = $downPer;
$_SESSION['delPer'] = $delPer;
$_SESSION['upPer'] = $upPer;
$login_successful = true;
break;
}
}
$result->free_result();
$stmt->close();
$conn->close();
if ($login_successful) {
header("Location: ftp/index.php");
exit();
} else {
// If password verification fails
$_SESSION['login_error'] = "Invalid username or password.";
header("Location: ../index.php");
exit();
}
} else {
// If no user is found
$result->free_result();
$stmt->close();
$conn->close();
$_SESSION['login_error'] = "Invalid username or password.";
header("Location: ../index.php");
exit();
}
} else {
// If form fields are not set
$_SESSION['login_error'] = "Please fill in all fields.";
header("Location: ../index.php");
exit();
if (!isset($_POST['uname'], $_POST['pswd'])
|| !is_string($_POST['uname']) || !is_string($_POST['pswd'])) {
$_SESSION['login_error'] = 'Please fill in all fields.';
header('Location: ../index.php');
exit;
}
?>
$username = trim($_POST['uname']);
$password = $_POST['pswd'];
$loginSuccessful = false;
$database = getDatabaseConnection();
try {
$statement = $database->prepare('SELECT uname, pswd, admin, defPath, downPer, delPer, upPer FROM users WHERE uname = ?');
$statement->bind_param('s', $username);
$statement->execute();
$result = $statement->get_result();
while ($user = $result->fetch_assoc()) {
if (!password_verify($password, $user['pswd'])) {
continue;
}
session_regenerate_id(true);
$_SESSION['uname'] = $user['uname'];
$_SESSION['admin'] = (bool) $user['admin'];
$_SESSION['defPath'] = $user['defPath'];
foreach (['downPer', 'delPer', 'upPer'] as $permission) {
$_SESSION[$permission] = (bool) $user[$permission];
}
unset($_SESSION['login_error']);
$loginSuccessful = true;
break;
}
$result->free();
$statement->close();
if (!$loginSuccessful) {
$_SESSION['login_error'] = 'Invalid username or password.';
}
} catch (mysqli_sql_exception $exception) {
error_log($exception->getMessage());
$_SESSION['login_error'] = 'Database error. Please try again.';
} finally {
$database->close();
}
header('Location: ' . ($loginSuccessful ? 'ftp/index.php' : '../index.php'));
exit;
+11 -47
View File
@@ -1,47 +1,11 @@
html, body {
height: 100%;
margin: 0;
}
body {
display: flex;
flex-direction: column;
}
main {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding-top: 7%;
}
.fixed {
-ms-flex: 0 0 300px;
flex: 0 0 300px;
min-width: 300px;
}
.hover-effect {
transition: opacity 0.3s ease;
}
.hover-effect:hover {
opacity: 0.8;
}
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.theme-light .dark-logo {
display: none;
}
.theme-dark .light-logo {
display: none;
}
.login-page { display:flex; min-height:100dvh; flex-direction:column; }
.login-top,.login-footer { display:flex; align-items:center; justify-content:space-between; padding:28px 5vw; gap:16px; }
.login-brand { display:flex; align-items:center; gap:12px; font-size:19px; color:var(--text); font-weight:750; letter-spacing:-.03em; } .login-brand > span { display:grid; place-items:center; background:#2563eb; color:white; width:36px; height:36px; border-radius:9px; font-size:25px; }
.login-layout { flex:1; width:min(1060px,90%); margin:48px auto; display:grid; grid-template-columns:1.1fr 1fr; align-items:center; gap:100px; }
.login-story h1 { font-size:clamp(44px,5vw,64px); margin:16px 0 20px; line-height:1.08; letter-spacing:-.055em; } .login-story h1 > span { color:var(--blue); } .login-story > p { max-width:370px; color:var(--muted); font-size:15px; line-height:1.8; }
.login-card { background:var(--surface); padding:40px; border:1px solid var(--line); border-radius:18px; box-shadow:var(--shadow); } .login-card h2 { margin:12px 0 8px; font-size:24px; } .login-card form { margin-top:30px; } .login-card label { font-size:12px; } .login-card .btn { width:100%; justify-content:space-between; padding:13px 16px; } .login-note { margin-top:28px; padding-top:22px; border-top:1px solid var(--line); font-size:11px; color:var(--muted); text-align:center; }
.storage-illustration { max-width:310px; margin-top:35px; padding:18px; border:1px solid var(--line); border-radius:14px; background:var(--surface); transform:rotate(-3deg); box-shadow:12px 14px 0 var(--blue-soft); }
.storage-top { display:flex; justify-content:space-between; font-size:10px; font-weight:700; letter-spacing:.12em; margin-bottom:14px; } .storage-top span { color:var(--blue); } .storage-drive { display:flex; align-items:center; gap:16px; height:38px; padding:0 12px; border:1px solid var(--line); border-radius:6px; margin:6px 0; background:var(--bg); } .storage-drive b { font:10px ui-monospace,monospace; color:var(--muted); } .storage-drive > span { flex:1; height:5px; border-radius:4px; background:var(--line); } .storage-drive i { width:5px; height:5px; border-radius:50%; background:var(--blue); } .storage-bottom { color:var(--muted); font-size:7px; letter-spacing:.14em; margin-top:14px; }
.login-footer { font-size:11px; } .login-footer a { color:var(--muted); }
@media(max-width:800px) { .login-layout { grid-template-columns:1fr 1fr; gap:30px; } .login-card { padding:26px; } }
@media(max-width:620px) { .login-layout { display:block; max-width:430px; margin:24px auto; } .login-story { margin-bottom:30px; } .login-story h1 { font-size:38px; } .login-story h1 br { display:none; } .storage-illustration { display:none; } .login-story > p { font-size:13px; } .login-top,.login-footer { padding:20px; } .login-footer { flex-wrap:wrap; } }
+71 -181
View File
@@ -1,181 +1,71 @@
/* Modern dark theme with blue accents */
:root {
--bg-dark: #171b22;
--bg-dark2: #1a202c;
--primary-blue: #3b82f6;
--text-light: #e5e7eb;
--bg-darkblue: #101722;
--container-bg-start: #1a2029;
--container-bg-end: #151d29;
--sftp-bg-start: #151b25;
--sftp-bg-end: #101722;
--container-border: rgba(77, 145, 255, 0.7);
--panel-text: #e5e7eb;
--panel-muted: #aab4c3;
--control-bg: #101722;
--control-border: #39485d;
}
body {
background: linear-gradient(135deg, var(--bg-dark), var(--bg-darkblue));
color: var(--text-light);
min-height: 100vh;
font-family: system-ui, sans-serif;
margin: 0;
padding: 0;
}
a, .nav-link {
color: var(--primary-blue) !important;
transition: opacity .2s ease-in-out;
}
body, button, input, select, textarea {
font-family: system-ui, sans-serif;
}
/* Keep text, fields and tables readable on every page that uses the shared theme. */
.surface-panel,
.dark-box {
color: var(--panel-text);
}
.surface-panel .text-muted,
.dark-box .text-muted {
color: var(--panel-muted) !important;
}
.surface-panel .form-control,
.surface-panel .form-select,
.dark-box .form-control,
.dark-box .form-select {
color: var(--panel-text);
background-color: var(--control-bg);
border-color: var(--control-border);
}
.surface-panel .form-control::placeholder,
.dark-box .form-control::placeholder {
color: var(--panel-muted);
}
.surface-panel .form-control:focus,
.surface-panel .form-select:focus,
.dark-box .form-control:focus,
.dark-box .form-select:focus {
border-color: var(--primary-blue);
box-shadow: 0 0 0 .2rem rgba(59, 130, 246, .2);
}
.surface-panel .table,
.surface-panel .table th,
.surface-panel .table td {
color: inherit;
}
a:hover, .btn:hover, .nav-link:hover {
opacity: 0.85;
}
/* Ensure action buttons have white text for visibility */
.btn-primary,
.btn-danger,
.btn-warning,
.btn-success,
.btn-info,
.btn-secondary {
color: var(--text-light) !important;
}
/* Override specific actions (update password, upload directory, rename) to have black text */
.btn-primary,
.btn-warning,
.btn-info {
color: #212529 !important;
}
.dark-box {
background: rgba(0,0,0,0.6);
border-radius: var(--border-radius);
padding: 1rem;
backdrop-filter: blur(8px);
}
.surface-panel {
background: linear-gradient(145deg, var(--sftp-bg-start), var(--sftp-bg-end));
color: var(--text-light);
border: 1px solid rgba(77, 145, 255, 0.3);
border-radius: 1rem;
box-shadow: 0 12px 32px rgba(3, 8, 18, 0.2);
}
.modal-content {
background: rgba(20, 20, 30, 0.9) !important;
color: var(--text-light) !important;
border: none;
}
.table {
color: var(--text-light) !important;
}
.table th, .table td {
border-color: rgba(255,255,255,0.1) !important;
}
/* Main window styling (window within window) */
.custom-container.dark-box {
box-sizing: border-box;
margin: 2rem auto;
background: linear-gradient(135deg, var(--container-bg-start), var(--container-bg-end));
border: 2px solid var(--container-border);
border-radius: var(--border-radius);
padding: 1rem;
backdrop-filter: blur(6px);
}
/* Light mode overrides */
html[data-bs-theme="light"] {
--bg-light: #f8f9fa;
--bg-light2: #ffffff;
--container-bg: rgba(255,255,255,0.85);
--container-border: var(--primary-blue);
--text-dark: #212529;
--panel-text: #212529;
--panel-muted: #596579;
--control-bg: #ffffff;
--control-border: #c8d2df;
}
html[data-bs-theme="light"] body {
background: var(--bg-light);
color: var(--text-dark);
}
html[data-bs-theme="light"] .dark-box {
background: rgba(255, 255, 255, 0.8);
color: var(--text-dark);
}
html[data-bs-theme="light"] .surface-panel .form-control,
html[data-bs-theme="light"] .surface-panel .form-select,
html[data-bs-theme="light"] .dark-box .form-control,
html[data-bs-theme="light"] .dark-box .form-select {
color: var(--panel-text);
background-color: var(--control-bg);
border-color: var(--control-border);
}
html[data-bs-theme="light"] .form-control::placeholder {
color: var(--panel-muted);
}
html[data-bs-theme="light"] .surface-panel {
background: linear-gradient(145deg, #ffffff, #edf2f8);
color: var(--text-dark);
border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 12px 32px rgba(31, 58, 92, 0.08);
}
html[data-bs-theme="light"] .modal-content {
background: #fff !important;
color: var(--text-dark) !important;
}
html[data-bs-theme="light"] .table {
color: var(--text-dark) !important;
}
/* Shared UI primitives, independent of a CSS framework. */
:root { color-scheme: light; --bg:#f5f7fb; --surface:#fff; --surface-alt:#f0f4fa; --text:#17243b; --muted:#64748b; --line:#e0e7f0; --blue:#2563eb; --blue-soft:#eaf1ff; --danger:#c83b50; --success:#168364; --shadow:0 16px 48px #16294a09; --body-bg-rgb:245,247,251; }
:root[data-theme="dark"] { color-scheme:dark; --bg:#0c1220; --surface:#121c2d; --surface-alt:#19253a; --text:#e4ecfa; --muted:#96a6bf; --line:#26334a; --blue:#79a7ff; --blue-soft:#1c3053; --danger:#ff8797; --success:#64d3ae; --shadow:0 16px 48px #0002; --body-bg-rgb:12,18,32; }
* { box-sizing:border-box; }
body { margin:0; min-height:100vh; background:var(--bg); color:var(--text); font:14px/1.55 'Segoe UI',system-ui,sans-serif; }
button,input,select,textarea { font:inherit; }
a { color:var(--blue); text-decoration:none; } a:hover { text-decoration:underline; }
:focus-visible { outline:3px solid var(--blue); outline-offset:3px; }
[hidden] { display:none !important; }
h1,h2,h3,h4,h5,p { margin:0 0 1rem; }
h1 { font-size:clamp(26px,3vw,36px); letter-spacing:-.045em; line-height:1.2; }
h2 { font-size:23px; letter-spacing:-.03em; } h3 { font-size:18px; } h4,h5 { font-size:16px; }
small,.small,.form-text { font-size:12px; }
.text-muted,.form-text { color:var(--muted); } .text-danger { color:var(--danger); } .text-success { color:var(--success); }
.text-center { text-align:center; } .text-end { text-align:right; } .text-break,.text-wrap { overflow-wrap:anywhere; } .fw-bold { font-weight:650; }
.btn { display:inline-flex; justify-content:center; align-items:center; gap:8px; min-height:38px; padding:8px 14px; border:1px solid var(--line); border-radius:8px; background:var(--surface); color:var(--text); cursor:pointer; font-weight:600; line-height:1.4; text-decoration:none; transition:background .15s,border-color .15s; }
.btn:hover { background:var(--surface-alt); border-color:var(--muted); text-decoration:none; }
.btn-primary { background:#2563eb; border-color:#2563eb; color:#fff; } .btn-primary:hover { background:#1d4ed8; border-color:#1d4ed8; }
.btn-danger { color:var(--danger); } .btn-success,.btn-info { color:var(--blue); }
.btn-sm { min-height:32px; padding:6px 10px; font-size:12px; } .btn:disabled,.btn.disabled { opacity:.5; cursor:default; }
.btn-group,.input-group { display:flex; gap:8px; }
.form-control,.form-select { display:block; width:100%; min-width:0; border:1px solid var(--line); border-radius:8px; background:var(--surface); color:var(--text); padding:11px 13px; }
.form-control::placeholder { color:var(--muted); } .form-control:focus,.form-select:focus { border-color:var(--blue); outline:3px solid var(--blue-soft); }
label,.form-label { display:inline-block; margin-bottom:6px; font-weight:600; } input[type="checkbox"] { accent-color:var(--blue); width:16px; height:16px; }
.form-check { display:flex; align-items:center; gap:8px; } .form-check label { margin:0; }
.surface-panel,.card { background:var(--surface); border:1px solid var(--line); border-radius:12px; padding:24px; }
.card-header { border-bottom:1px solid var(--line); padding:12px; } .card-body { padding:20px; }
.custom-container.dark-box { width:calc(100% - 48px); max-width:1280px; min-height:calc(100vh - 100px); margin:0 auto 24px; padding:24px; background:var(--surface); border:1px solid var(--line); border-radius:16px; box-shadow:var(--shadow); }
.custom-container > header { padding-bottom:20px; margin:8px 0 24px; }
table { width:100%; border-collapse:collapse; text-align:left; } th { color:var(--muted); font-size:11px; font-weight:650; letter-spacing:.06em; text-transform:uppercase; background:var(--surface-alt); }
th,td { padding:14px 16px; border-bottom:1px solid var(--line); overflow-wrap:anywhere; } tbody tr:hover { background:var(--surface-alt); }
.alert { padding:14px 16px; margin:16px 0; border:1px solid var(--line); border-radius:8px; background:var(--blue-soft); text-align:left; } .alert-danger { color:var(--danger); } .alert-success { color:var(--success); }
.badge { display:inline-block; padding:2px 7px; border-radius:5px; background:var(--surface-alt); color:var(--muted); font-size:10px; font-weight:600; }
.progress { height:8px; background:var(--surface-alt); border-radius:8px; overflow:hidden; } .progress-bar { height:100%; background:var(--blue); }
.bg-success { background:var(--success); } .bg-warning { background:#d99b32; } .bg-danger { background:var(--danger); }
.badge.bg-danger,.badge.bg-success,.badge.bg-warning { background:var(--surface-alt); color:var(--muted); }
.img-fluid { max-width:100%; height:auto; }
html:not([data-theme="dark"]) .dark-logo,[data-theme="dark"] .light-logo { display:none; }
footer { color:var(--muted); font-size:12px; } footer img { opacity:.7; max-height:22px; object-fit:contain; }
.theme-toggle { white-space:nowrap; }
.d-flex { display:flex; } .flex-column { flex-direction:column; } .flex-wrap { flex-wrap:wrap; } .justify-content-end { justify-content:flex-end; } .justify-content-center { justify-content:center; } .justify-content-between { justify-content:space-between; } .align-items-center { align-items:center; } .gap-3 { gap:16px; }
.row { display:flex; flex-wrap:wrap; gap:20px; } .col { flex:1; min-width:0; } .col-12 { width:100%; } .col-4 { flex:1; }
.container,.container-fluid { width:100%; margin-inline:auto; } .container { max-width:1280px; }
.border { border:1px solid var(--line); } .border-top { border-top:1px solid var(--line); } .border-bottom { border-bottom:1px solid var(--line); } .rounded { border-radius:10px; }
.position-relative { position:relative; } .w-100 { width:100%; } .h-100 { height:100%; } .w-auto { width:auto; } .mx-auto { margin-inline:auto; }
.p-2 { padding:12px; } .p-3 { padding:20px; } .m-3 { margin:20px; } .m-5 { margin:32px; }
.mb-1,.mt-1 { margin-block:4px; } .mb-2 { margin-bottom:8px; } .mb-3 { margin-bottom:16px; } .mb-4 { margin-bottom:24px; } .mt-2 { margin-top:8px; } .mt-3 { margin-top:16px; } .mt-4 { margin-top:24px; } .me-2 { margin-right:8px; }
dialog.modal { width:min(480px,calc(100% - 32px)); padding:0; border:1px solid var(--line); border-radius:16px; background:var(--surface); color:var(--text); box-shadow:0 24px 100px #0005; max-height:calc(100dvh - 40px); }
dialog::backdrop { background:#080f2199; backdrop-filter:blur(4px); }
.modal-header,.modal-footer { padding:20px 24px; display:flex; align-items:center; justify-content:space-between; gap:12px; } .modal-header { border-bottom:1px solid var(--line); } .modal-title { margin:0; } .modal-body { padding:24px; } .modal-footer { justify-content:flex-end; border-top:1px solid var(--line); }
.btn-close { border:0; background:transparent; color:var(--muted); cursor:pointer; font-size:24px; } .btn-close::before { content:'×'; } body:has(dialog[open]) { overflow:hidden; }
.eyebrow { color:var(--muted); font-size:10px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; }
@media(min-width:900px) { .col-md-6 { width:calc(50% - 10px); } .col-lg-4 { width:calc(33.333% - 14px); } }
@media(max-width:700px) { .custom-container.dark-box { width:calc(100% - 24px); padding:16px; } .surface-panel { overflow-x:auto; } .row { gap:12px; } .col-md-6,.col-lg-4 { width:100%; } th,td { padding:10px; } .m-5 { margin:16px; } }
@media(prefers-reduced-motion:reduce) { *,*::before,*::after { transition:none !important; animation:none !important; } }
/* Small local icon vocabulary used by secondary screens. */
.ui-icon { font-style:normal; display:inline-block; }
.icon-chevron-left::before { content:'‹'; } .icon-chevron-right::before { content:'›'; }
.icon-fullscreen::before { content:'⛶'; } .icon-fullscreen-exit::before { content:'⊡'; }
.icon-share::before { content:'↗'; } .icon-clipboard::before { content:'▤'; }
.icon-check::before,.icon-check-circle-fill::before { content:'✓'; }
.icon-x-circle-fill::before { content:'×'; } .icon-exclamation-triangle::before { content:'!'; }
.icon-server::before,.icon-hdd::before,.icon-hdd-stack::before { content:'▤'; }
.icon-clock::before { content:'◷'; } .icon-cpu::before { content:'▦'; } .icon-memory::before { content:'▥'; }
.text-primary { color:var(--blue); } .text-warning { color:#c98d22; }
.spinner-border { display:inline-block; width:14px; height:14px; border:2px solid var(--line); border-top-color:var(--blue); border-radius:50%; animation:spin .8s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
/* Secondary screens share the same surfaces and spacing. */
.col-lg-10 { width:100%; } .py-4 { padding-block:24px; } .mb-0 { margin-bottom:0; } .h4 { font-size:22px; } .h5 { font-size:16px; }
.card-header.bg-success,.card-header.bg-warning,.card-header.bg-danger,.card-header.bg-primary,.card-header.bg-info { background:var(--surface-alt); }
.custom-container.dark-box.fullscreen-mode { max-width:none; width:100%; height:100dvh; margin:0; border:0; border-radius:0; padding:0; }
+62
View File
@@ -0,0 +1,62 @@
/* Persistent workspace frame shared by every signed-in page and shared previews. */
.file-app { display:grid; grid-template-columns:236px minmax(0,1fr); text-align:left; }
.skip-link { position:fixed; left:250px; top:-60px; z-index:10000; padding:10px 16px; background:var(--surface); border:1px solid var(--blue); border-radius:8px; }
.skip-link:focus { top:12px; }
.sidebar { position:sticky; top:0; height:100dvh; padding:30px 20px 16px; display:flex; flex-direction:column; background:var(--surface); border-right:1px solid var(--line); overflow-y:auto; }
.brand { display:flex; align-items:center; gap:12px; color:var(--text); font-size:19px; font-weight:750; letter-spacing:-.04em; text-decoration:none; }
.brand-mark { display:grid; place-items:center; width:40px; height:40px; border-radius:11px; background:#2563eb; color:white; font-size:27px; }
.brand-caption { display:block; font-size:9px; font-weight:600; letter-spacing:.16em; color:var(--muted); margin-top:1px; }
.nav-heading { margin:48px 12px 12px; }
.nav-item { display:flex; align-items:center; gap:12px; padding:11px 12px; margin:4px 0; color:var(--muted); border-radius:8px; font-weight:550; }
.nav-item:hover { text-decoration:none; background:var(--surface-alt); color:var(--text); }
.nav-item.active { background:var(--blue-soft); color:var(--blue); }
.nav-item > span { font-size:20px; width:21px; text-align:center; }
.sidebar-bottom { margin-top:auto; padding-top:40px; }
.connection-card { border:1px solid var(--line); border-radius:10px; padding:14px 12px; font-size:12px; font-weight:600; margin-bottom:18px; }
.status-dot { display:inline-block; width:6px; height:6px; background:var(--success); border-radius:50%; margin-right:7px; }
.connection-card small { display:block; margin-top:5px; color:var(--muted); font-weight:400; font-size:10px; }
.user-card { display:flex; gap:10px; align-items:center; padding:16px 4px 0; margin-top:14px; border-top:1px solid var(--line); }
.avatar { display:grid; place-items:center; background:var(--blue-soft); color:var(--blue); width:34px; height:34px; border-radius:50%; font-weight:700; flex-shrink:0; }
.user-name { flex:1; overflow-wrap:anywhere; font-weight:600; font-size:12px; }
.user-name small { display:block; font-size:10px; font-weight:400; color:var(--muted); }
.workspace { min-width:0; display:flex; flex-direction:column; min-height:100dvh; }
.topbar { min-height:76px; display:flex; justify-content:space-between; align-items:center; gap:12px; padding:16px 40px; border-bottom:1px solid var(--line); background:var(--surface); color:var(--muted); font-size:12px; }
.topbar strong { color:var(--text); font-weight:500; }
.topbar-divider { padding:0 14px; opacity:.5; }
.file-main { flex:1; width:100%; max-width:1560px; margin:0 auto; padding:42px 40px 0; display:flex; flex-direction:column; min-width:0; }
.workspace-content { flex:1; min-width:0; }
.page-heading { display:flex; align-items:center; justify-content:space-between; gap:20px; margin-bottom:30px; }
.page-heading .eyebrow { margin-bottom:9px; }
.page-heading h1 { margin-bottom:10px; font-size:36px; overflow-wrap:anywhere; }
.heading-dot { color:var(--blue); }
.page-heading p { color:var(--muted); margin:0; font-size:13px; }
.heading-back { white-space:nowrap; font-size:12px; flex-shrink:0; }
.protocol-badge { border:1px solid var(--line); border-radius:6px; padding:5px 10px; font:11px ui-monospace,monospace; background:var(--surface); white-space:nowrap; }
.workspace-footer { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:22px 0; font-size:10px; margin:0; border:0; }
.workspace-footer a { color:var(--muted); }
.content-panel { padding:24px; border:1px solid var(--line); border-radius:12px; background:var(--surface); box-shadow:var(--shadow); min-width:0; }
.panel-heading { margin-bottom:22px; } .panel-heading h2 { margin-bottom:6px; font-size:18px; } .panel-heading p { margin:0; color:var(--muted); font-size:12px; }
.content-toolbar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; margin-bottom:20px; font-size:12px; color:var(--muted); }
.content-toolbar p { margin:0; }
.table-scroll { overflow-x:auto; max-width:100%; }
@media(min-width:1500px) { .file-main { padding-top:56px; } }
@media(max-width:1100px) { .file-app { grid-template-columns:200px minmax(0,1fr); } .sidebar { padding-inline:14px; } .nav-item { font-size:13px; } .file-main { padding:28px 24px 0; } .topbar { padding-inline:24px; } }
@media(max-width:760px) {
.file-app { display:block; }
.sidebar { position:static; height:auto; padding:16px; border-right:0; border-bottom:1px solid var(--line); overflow:visible; }
.brand-caption,.nav-heading,.connection-card { display:none; }
.sidebar nav { display:flex; flex-wrap:wrap; gap:6px; margin-top:14px; }
.nav-item { font-size:12px; padding:7px 10px; margin:0; }
.sidebar-bottom { margin:8px 0 0; padding:0; display:flex; }
.sidebar-bottom .nav-item { padding:4px 10px; }
.sidebar-bottom .user-card { display:flex; margin:0 0 0 auto; padding:0 10px; border:0; }
.sidebar-bottom .user-name,.sidebar-bottom .avatar { display:none; }
.topbar { min-height:58px; padding:12px 16px; }
.file-main { padding:26px 16px 0; }
.page-heading { align-items:flex-start; flex-wrap:wrap; }
.page-heading h1 { font-size:30px; }
.protocol-badge { display:none; }
.workspace-footer { flex-wrap:wrap; }
.content-panel { padding:18px; }
.skip-link { left:16px; }
}
+28
View File
@@ -0,0 +1,28 @@
<?php
/** Shared session checks. Database, form and session keys retain their legacy names. */
function requireLogin(string $loginUrl): void {
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
if (!isset($_SESSION['uname'])) {
header('Location: ' . $loginUrl);
exit;
}
}
function hasPermission(string $permission): bool {
return !empty($_SESSION[$permission]);
}
function requirePermission(string $permission, string $message): void {
if (!hasPermission($permission)) {
http_response_code(403);
exit($message);
}
}
function isPasswordValid(string $password): bool {
return strlen($password) >= 8
&& preg_match('/[0-9]/', $password) === 1
&& preg_match('/[A-Z]/', $password) === 1;
}
+46
View File
@@ -0,0 +1,46 @@
<?php
/** SFTP paths use POSIX separators, regardless of the OS running PHP. */
function normalizePath(string $path): string {
$parts = array_filter(explode('/', $path), fn($part) => $part !== '' && $part !== '.');
$stack = [];
foreach ($parts as $part) {
if ($part === '..') {
array_pop($stack);
} else {
$stack[] = $part;
}
}
return '/' . implode('/', $stack);
}
/** Like dirname(), but only '/' is a separator and the root stays '/'. */
function remoteDirname(string $path): string {
$path = rtrim($path, '/');
if ($path === '') return '/';
$separator = strrpos($path, '/');
if ($separator === false) return '.';
$parent = rtrim(substr($path, 0, $separator), '/');
return $parent === '' ? '/' : $parent;
}
/** Compare directory boundaries, so /data-other is not considered part of /data. */
function isRemotePathWithin(string $path, string $basePath): bool {
$path = normalizePath($path);
$basePath = normalizePath($basePath);
return $path === $basePath || strpos($path, rtrim($basePath, '/') . '/') === 0;
}
/** Validate a single POSIX filename before joining it to a directory. */
function isRemoteNameValid(string $name): bool {
return $name !== '' && $name !== '.' && $name !== '..'
&& strpos($name, '/') === false && strpos($name, "\0") === false;
}
/** Resolve a request path inside the user's configured workspace. */
function requireRemotePath(string $path, string $basePath): string {
if (strpos($path, "\0") !== false || !isRemotePathWithin($path, $basePath)) {
http_response_code(403);
exit('Path is outside your workspace.');
}
return normalizePath($path);
}
+66
View File
@@ -0,0 +1,66 @@
<?php
/** Shared navigation and page frame for the SFTP workspace. */
function workspaceEscape($value) {
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function workspaceAsset($root, $path) {
return workspaceEscape($root . $path . '?v=' . filemtime(__DIR__ . '/../' . $path));
}
function renderWorkspaceStart(array $page) {
$root = $page['root'];
$active = $page['active'] ?? 'files';
$guest = $page['guest'] ?? false;
$username = $_SESSION['uname'] ?? '';
$admin = !$guest && !empty($_SESSION['admin']);
$filesUrl = $root . 'content/ftp/index.php';
$nav = [
'files' => ['All files', '▱', $filesUrl],
'server' => ['Server status', '◴', $root . 'content/ftp/serverstat.php'],
'users' => ['User management', '♙', $root . 'content/adminpanel.php'],
];
?>
<body class="file-app">
<a class="skip-link" href="#workspace-content">Skip to content</a>
<aside class="sidebar">
<a class="brand" href="<?= workspaceEscape($guest ? $root . 'index.php' : $filesUrl) ?>"><span class="brand-mark" aria-hidden="true">▤</span><span>USB RAID<span class="brand-caption">SFTP WORKSPACE</span></span></a>
<div class="eyebrow nav-heading"><?= $guest ? 'Shared workspace' : 'Workspace' ?></div>
<nav aria-label="Main navigation">
<?php if ($guest): ?>
<a class="nav-item active" href="#workspace-content" aria-current="page"><span aria-hidden="true">↗</span> Shared file</a>
<?php else: ?>
<?php foreach ($nav as $key => [$label, $icon, $url]): ?>
<?php if ($key !== 'files' && !$admin) continue; ?>
<a class="nav-item<?= $active === $key ? ' active' : '' ?>" href="<?= workspaceEscape($url) ?>"<?= $active === $key ? ' aria-current="page"' : '' ?>><span aria-hidden="true"><?= $icon ?></span> <?= $label ?></a>
<?php endforeach; ?>
<?php endif; ?>
</nav>
<div class="sidebar-bottom">
<div class="connection-card"><?php if (!empty($page['connected'])): ?><span class="status-dot"></span><?php endif; ?><?= $guest ? 'Shared access' : (!empty($page['connected']) ? 'Server connected' : 'Remote storage') ?><small><?= $guest ? 'Access through a shared link' : 'Your SFTP workspace' ?></small></div>
<?php if ($guest): ?>
<a class="nav-item" href="<?= workspaceEscape($root . 'index.php') ?>">Sign in to your workspace ↗</a>
<?php else: ?>
<a class="nav-item<?= $active === 'account' ? ' active' : '' ?>" href="<?= workspaceEscape($root . 'content/changepassword.php') ?>"<?= $active === 'account' ? ' aria-current="page"' : '' ?>>Account settings</a>
<div class="user-card"><span class="avatar"><?= workspaceEscape(strtoupper(substr($username, 0, 1))) ?></span><span class="user-name"><?= workspaceEscape($username) ?><small><?= $admin ? 'Administrator' : 'Member' ?></small></span><a href="<?= workspaceEscape($root . 'content/logout.php') ?>" title="Sign out" aria-label="Sign out">↗</a></div>
<?php endif; ?>
</div>
</aside>
<div class="workspace">
<header class="topbar"><span><?= $guest ? 'Shared workspace' : 'Workspace' ?><span class="topbar-divider">/</span><strong><?= workspaceEscape($page['section']) ?></strong></span><button id="themeToggle" class="btn btn-sm theme-toggle"><i aria-hidden="true"></i><span id="themeText">Theme</span></button></header>
<main class="file-main" id="workspace-content" tabindex="-1">
<div class="page-heading"><div><div class="eyebrow"><?= workspaceEscape($page['eyebrow']) ?></div><h1><?= workspaceEscape($page['title']) ?><span class="heading-dot">.</span></h1><p><?= workspaceEscape($page['description']) ?></p></div>
<?php if (isset($page['back'])): ?><a class="btn heading-back" href="<?= workspaceEscape($page['back']) ?>">← Back to files</a><?php elseif (!empty($page['connected'])): ?><span class="protocol-badge"><span class="status-dot"></span> SFTP</span><?php endif; ?>
</div>
<div class="workspace-content <?= workspaceEscape($page['class'] ?? '') ?>">
<?php
}
function renderWorkspaceEnd() {
?>
</div>
<footer class="workspace-footer"><span>USB RAID Array <span aria-hidden="true">·</span> Developed by Michal Sedlák</span><a href="https://github.com/michalcz10/USB-RAID-Array" target="_blank" rel="noopener noreferrer">View on GitHub ↗</a></footer>
</main>
</div>
<?php
}
+12 -58
View File
@@ -15,68 +15,22 @@ if (empty($_SESSION)) {
?>
<!DOCTYPE html>
<html lang="cz">
<html lang="en">
<head>
<title>Login</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="css/bootstrap.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign in · USB RAID</title>
<link rel="icon" href="img/favicon.ico">
<link rel="stylesheet" href="css/modern.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="css/index.css">
<script src="js/bootstrap.bundle.js"></script>
</head>
<body class="container-fluid text-center">
<div class="d-flex justify-content-end p-3">
<button id="themeToggle" class="btn btn-sm theme-toggle">
<i class="bi"></i>
<span id="themeText"></span>
</button>
</div>
<div class="custom-container dark-box">
<header class="row border-bottom m-5">
<h1>USB RAID Array</h1>
</header>
<main>
<section class="row m-3">
<article class="col border border-2 border-primary rounded p-2 fixed surface-panel">
<form action="content/login.php" method="post">
<div class="mb-3">
<label for="uname">Username:</label>
<br>
<input type="text" name="uname" class="form-control" required>
</div>
<div class="mb-3">
<label for="pswd">Password:</label>
<br>
<input type="password" name="pswd" class="form-control" required>
</div>
<div class="mb-3">
<input type="submit" class="btn btn-primary" value="Login">
</div>
</form>
</article>
</section>
</main>
<footer class="d-flex flex-column justify-content-center align-items-center p-3 border-top gap-3">
<span class="text-muted">Developed by Michal Sedlák</span>
<div class="d-flex gap-3">
<a href="https://github.com/michalcz10/USB-RAID-Array" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="img/GitHub_Logo.png" alt="GitHub Logo" class="img-fluid hover-effect light-logo" style="height: 32px;">
<img src="img/GitHub_Logo_White.png" alt="GitHub Logo" class="img-fluid hover-effect dark-logo" style="height: 32px;">
</a>
<a href="https://app.freelo.io/public/shared-link-view/?a=81efbcb4df761b3f29cdc80855b41e6d&b=4519c717f0729cc8e953af661e9dc981" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="img/freelo-logo-rgb.png" alt="Freelo Logo" class="img-fluid hover-effect light-logo" style="height: 24px;">
<img src="img/freelo-logo-rgb-on-dark.png" alt="Freelo Logo" class="img-fluid hover-effect dark-logo" style="height: 24px;">
</a>
</div>
</footer>
</div>
<script src="js/theme.js"></script>
</head>
<body class="login-page">
<header class="login-top"><a class="login-brand" href="index.php"><span>▤</span> USB RAID</a><button id="themeToggle" class="btn btn-sm"><i aria-hidden="true"></i><span id="themeText">Theme</span></button></header>
<main class="login-layout">
<section class="login-story"><div class="eyebrow">Your storage. Your workspace.</div><h1>A home for<br>every file<span>.</span></h1><p>Connect to your RAID storage and keep your files within reach. One simple workspace, powered by SFTP.</p><div class="storage-illustration" aria-hidden="true"><div class="storage-top">USB RAID <span>SFTP</span></div><div class="storage-drive"><b>01</b><span></span><i></i></div><div class="storage-drive"><b>02</b><span></span><i></i></div><div class="storage-drive"><b>03</b><span></span><i></i></div><div class="storage-bottom">REMOTE STORAGE / LOCAL CONTROL</div></div></section>
<section class="login-card"><div class="eyebrow">Welcome back</div><h2>Sign in to your workspace</h2><p class="text-muted">Use your account to access your files.</p><form action="content/login.php" method="post"><div class="mb-3"><label for="uname">Username</label><input id="uname" name="uname" class="form-control" autocomplete="username" placeholder="Enter your username" required></div><div class="mb-4"><label for="pswd">Password</label><input id="pswd" type="password" name="pswd" class="form-control" autocomplete="current-password" placeholder="Enter your password" required></div><button type="submit" class="btn btn-primary">Sign in <span aria-hidden="true">→</span></button></form><div class="login-note">Need access? Contact your administrator.</div></section>
</main>
<footer class="login-footer"><span>Developed by Michal Sedlák</span><a href="https://github.com/michalcz10/USB-RAID-Array" target="_blank" rel="noopener noreferrer">Project on GitHub ↗</a></footer>
</body>
</html>
+22 -36
View File
@@ -1,40 +1,26 @@
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.getElementById('themeToggle');
const html = document.documentElement;
const themeText = document.getElementById('themeText');
const themeIcon = themeToggle.querySelector('.bi');
(() => {
const preference = window.matchMedia('(prefers-color-scheme: dark)');
let saved;
try { saved = localStorage.getItem('theme'); } catch (_) { /* Storage may be disabled. */ }
function setTheme(theme) {
html.setAttribute('data-bs-theme', theme);
document.body.classList.remove('theme-light', 'theme-dark');
document.body.classList.add('theme-' + theme);
localStorage.setItem('theme', theme);
if (theme === 'dark') {
themeText.textContent = 'Light Mode';
themeIcon.className = 'bi bi-sun';
themeToggle.classList.remove('btn-dark');
themeToggle.classList.add('btn-light');
} else {
themeText.textContent = 'Dark Mode';
themeIcon.className = 'bi bi-moon';
themeToggle.classList.remove('btn-light');
themeToggle.classList.add('btn-dark');
document.documentElement.dataset.theme = theme;
const button = document.getElementById('themeToggle');
if (button) {
button.setAttribute('aria-label', `Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`);
const label = document.getElementById('themeText');
if (label) label.textContent = theme === 'dark' ? 'Light mode' : 'Dark mode';
const icon = button.querySelector('i');
if (icon) { icon.textContent = theme === 'dark' ? '☀' : '◐'; icon.setAttribute('aria-hidden', 'true'); }
}
}
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme) {
setTheme(savedTheme);
} else {
setTheme(prefersDark ? 'dark' : 'light');
}
themeToggle.addEventListener('click', function() {
const currentTheme = html.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
setTheme(saved === 'dark' || saved === 'light' ? saved : preference.matches ? 'dark' : 'light');
document.addEventListener('DOMContentLoaded', () => {
setTheme(document.documentElement.dataset.theme);
document.getElementById('themeToggle')?.addEventListener('click', () => {
saved = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
setTheme(saved);
try { localStorage.setItem('theme', saved); } catch (_) { /* Keep this session usable. */ }
});
});
});
preference.addEventListener('change', () => { if (saved !== 'dark' && saved !== 'light') setTheme(preference.matches ? 'dark' : 'light'); });
})();
+10
View File
@@ -0,0 +1,10 @@
// Native dialogs supply focus containment, Escape and focus restoration.
document.addEventListener('click', event => {
const trigger = event.target.closest('[data-dialog]');
if (trigger) document.getElementById(trigger.dataset.dialog)?.showModal();
if (event.target.closest('[data-dialog-close]')) event.target.closest('dialog')?.close();
if (event.target instanceof HTMLDialogElement) {
const rect = event.target.getBoundingClientRect();
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) event.target.close();
}
});
+147
View File
@@ -0,0 +1,147 @@
<?php
require_once __DIR__ . '/../Web/includes/auth.php';
function expectBackend(bool $condition, string $message): void {
if (!$condition) throw new RuntimeException($message);
}
// Run handlers in separate processes because redirects and permission checks exit.
if (($argv[1] ?? '') === '--handler') {
session_set_save_handler(new class implements SessionHandlerInterface {
public function open(string $path, string $name): bool { return true; }
public function close(): bool { return true; }
public function read(string $id): string|false { return ''; }
public function write(string $id, string $data): bool { return true; }
public function destroy(string $id): bool { return true; }
public function gc(int $maxLifetime): int|false { return 0; }
}, true);
session_start(['use_cookies' => false, 'cache_limiter' => '']);
http_response_code(200);
$scenario = json_decode($argv[3], true, 512, JSON_THROW_ON_ERROR);
$_SESSION = $scenario['session'] ?? [];
$_POST = $scenario['post'] ?? [];
$_GET = $scenario['get'] ?? [];
$_FILES = $scenario['files'] ?? [];
$_SERVER['REQUEST_METHOD'] = $scenario['method'] ?? 'POST';
$defaultPath = '/mnt/raid';
$calls = [];
function initializeSFTP() {
global $calls;
$calls[] = ['connect'];
return new class {
function put($path, $data, $mode = null) {
global $calls;
$calls[] = $mode === null ? ['put', $path, $data] : ['put', $path, $data, $mode];
return true;
}
function is_dir($path) { return true; }
function mkdir($path) {
global $calls;
$calls[] = ['mkdir', $path];
return true;
}
};
}
// Minimal database double exercises login without a live database or network.
function getDatabaseConnection() {
return new class {
function prepare($sql) {
return new class {
function bind_param($types, &...$values) {}
function execute() {}
function get_result() {
return new class {
private $fetched = false;
function fetch_assoc() {
if ($this->fetched) return null;
$this->fetched = true;
return ['uname' => 'member', 'pswd' => password_hash(' Secret1<&" ', PASSWORD_BCRYPT),
'admin' => 0, 'defPath' => '/mnt/raid', 'downPer' => 1, 'delPer' => 0, 'upPer' => 1];
}
function free() {}
};
}
function close() {}
};
}
function close() {}
};
}
register_shutdown_function(function () use (&$calls) {
echo "\n__RESULT__" . json_encode(['status' => http_response_code(), 'calls' => $calls, 'session' => $_SESSION]);
});
$path = realpath(__DIR__ . '/../Web/content/' . $argv[2] . '.php');
$source = file_get_contents($path);
// Replace only configuration loading, retaining the real guards and handlers.
$source = preg_replace("~require_once __DIR__ \\. '/(?:\\.\\./)+config/config\\.php';~", '', $source);
$source = str_replace('__DIR__', var_export(dirname($path), true), $source);
$source = str_replace('session_start();', '', $source);
require_once __DIR__ . '/../Web/includes/remotepaths.php';
require_once __DIR__ . '/../Web/vendor/autoload.php';
eval('?>' . $source);
exit;
}
function runBackendHandler(string $handler, array $scenario): array {
$process = proc_open([PHP_BINARY, __FILE__, '--handler', $handler, json_encode($scenario)],
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
fclose($pipes[0]);
$output = stream_get_contents($pipes[1]);
$errors = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($process);
expectBackend($exitCode === 0 && $errors === '', $handler . ' failed: ' . $errors . $output);
[$body, $result] = explode("\n__RESULT__", $output, 2);
return ['body' => $body] + json_decode($result, true, 512, JSON_THROW_ON_ERROR);
}
foreach (['short1A' => false, 'lowercase1' => false, 'UppercaseOnly' => false, 'Password1' => true, ' Secret1<&" ' => true] as $password => $expected) {
expectBackend(isPasswordValid($password) === $expected, 'Incorrect password validation');
}
$member = ['uname' => 'member', 'upPer' => true, 'downPer' => true, 'delPer' => true];
foreach (['upload' => 'upPer', 'createdir' => 'upPer', 'createfile' => 'upPer', 'rename' => 'upPer', 'extract' => 'upPer', 'delete' => 'delPer', 'download' => 'downPer'] as $handler => $permission) {
$result = runBackendHandler('ftp/' . $handler, []);
expectBackend($result['calls'] === [] && $result['body'] === '', $handler . ' must require login before connecting');
$result = runBackendHandler('ftp/' . $handler, ['session' => ['uname' => 'member', $permission => false]]);
expectBackend($result['status'] === 403 && $result['calls'] === [], $handler . ' must enforce permissions before connecting');
}
$result = runBackendHandler('ftp/createfile', ['session' => $member,
'post' => ['action' => 'createFile', 'currentPath' => '/mnt/raid', 'fileName' => 'empty.txt']]);
expectBackend($result['status'] === 200 && $result['calls'][1] === ['put', '/mnt/raid/empty.txt', ''], 'New files must contain zero bytes');
expectBackend(json_decode($result['body'], true)['success'] === true, 'Create file response must retain its API');
foreach (['/mnt/raid-other', '/mnt/raid/../private'] as $path) {
$result = runBackendHandler('ftp/createdir', ['session' => $member,
'post' => ['action' => 'createDir', 'currentPath' => $path, 'dirName' => 'new']]);
expectBackend($result['status'] === 403 && count($result['calls']) === 1, 'Reject writes outside workspace');
}
$result = runBackendHandler('ftp/createdir', ['session' => $member, 'post' => ['action' => 'createDir', 'dirName' => '..']]);
expectBackend($result['status'] === 400 && count($result['calls']) === 1, 'Reject parent directory names');
$result = runBackendHandler('ftp/delete', ['session' => $member, 'post' => ['delete' => '/mnt/raid']]);
expectBackend($result['status'] === 400 && count($result['calls']) === 1, 'Protect workspace root from deletion');
$result = runBackendHandler('ftp/rename', ['session' => $member, 'post' => ['path' => '/mnt/raid/item', 'newName' => '../escape']]);
expectBackend($result['status'] === 400 && count($result['calls']) === 1, 'Rename must accept a single filename');
$upload = ['session' => $member, 'post' => ['paths' => ['folder/ok.txt', 'bad.txt']],
'files' => ['files' => ['name' => ['ok.txt', 'bad.txt'], 'tmp_name' => ['local-upload', ''],
'error' => [UPLOAD_ERR_OK, UPLOAD_ERR_PARTIAL]]]];
$result = runBackendHandler('ftp/upload', $upload);
expectBackend($result['status'] === 200 && $result['calls'][1] === ['put', '/mnt/raid/folder/ok.txt', 'local-upload', 1], 'Upload must use local file mode and preserve relative directories');
expectBackend(strpos($result['body'], 'File was only partially uploaded') !== false, 'Report errors for each uploaded file');
$upload['post']['paths'][0] = '../private.txt';
$result = runBackendHandler('ftp/upload', $upload);
expectBackend($result['status'] === 403 && count($result['calls']) === 1, 'Upload paths must stay in the selected directory');
$result = runBackendHandler('login', ['post' => ['uname' => 'member', 'pswd' => ' Secret1<&" ']]);
expectBackend(($result['session']['uname'] ?? '') === 'member', 'Login must preserve special characters and whitespace in passwords');
expectBackend($result['session']['admin'] === false && $result['session']['delPer'] === false && $result['session']['upPer'] === true, 'Login must retain boolean permissions');
$result = runBackendHandler('login', ['post' => ['uname' => 'member', 'pswd' => 'wrong']]);
expectBackend(!isset($result['session']['uname']) && isset($result['session']['login_error']), 'Reject incorrect passwords');
echo "Backend: handler permissions, path boundaries, empty files and login checks passed.\n";
+45
View File
@@ -0,0 +1,45 @@
<?php
require_once __DIR__ . '/../Web/includes/remotepaths.php';
$parents = [
'/' => '/',
'/etc' => '/',
'/etc/' => '/',
'/etc/file.txt' => '/etc',
'//etc//file.txt' => '//etc',
'/folder with spaces/item.txt' => '/folder with spaces',
'folder/file.txt' => 'folder',
'file.txt' => '.',
'/a\\b/file.txt' => '/a\\b', // Backslash is a valid POSIX filename character.
];
foreach ($parents as $path => $expected) {
if (remoteDirname($path) !== $expected) throw new RuntimeException('Incorrect parent for ' . $path);
}
foreach (['/' => '/', '//etc/../' => '/', '/etc/../../' => '/', '/etc/./ssh/' => '/etc/ssh'] as $path => $expected) {
if (normalizePath($path) !== $expected) throw new RuntimeException('Incorrect normalized path for ' . $path);
}
// Round trips must remain correct on both Windows/XAMPP and Linux.
foreach (['/etc', '/mnt/raid', '/folder with spaces'] as $directory) {
$parent = normalizePath(remoteDirname($directory));
if ($parent !== ($directory === '/mnt/raid' ? '/mnt' : '/')) throw new RuntimeException('Invalid navigation round trip');
}
// ZIP paths keep the top-level directory name when it is directly under root.
foreach (['/etc' => '/etc/config.txt', '/mnt/raid' => '/mnt/raid/config.txt'] as $base => $file) {
$relative = substr($file, strlen(rtrim(remoteDirname($base), '/')) + 1);
if ($relative !== ($base === '/etc' ? 'etc/config.txt' : 'raid/config.txt')) throw new RuntimeException('Incorrect ZIP member path');
}
foreach ([
['/mnt/raid', '/mnt/raid', true],
['/mnt/raid/folder', '/mnt/raid/', true],
['/mnt/raid-other/file', '/mnt/raid', false],
['/mnt/raid/../private', '/mnt/raid', false],
['/mnt/raid/sub/../../private', '/mnt/raid', false],
['/mnt/raid/sub/../file', '/mnt/raid', true],
['/etc', '/', true],
] as [$path, $basePath, $expected]) {
if (isRemotePathWithin($path, $basePath) !== $expected) throw new RuntimeException('Incorrect path boundary: ' . $path);
}
foreach (['' => false, '.' => false, '..' => false, 'a/b' => false, "a\0b" => false, 'file.txt' => true, '0' => true, 'a\\b' => true] as $name => $expected) {
if (isRemoteNameValid((string) $name) !== $expected) throw new RuntimeException('Incorrect filename validation');
}
echo "Remote paths: POSIX parents, normalization, workspace boundaries, names and ZIP paths passed.\n";
+57
View File
@@ -0,0 +1,57 @@
<?php
require_once __DIR__ . '/../Web/content/ftp/lib/serverstats.php';
require_once __DIR__ . '/../Web/includes/workspace.php';
// Treat warnings and deprecations as failures, including when rendering missing data.
set_error_handler(function ($severity, $message, $file, $line) {
throw new ErrorException($message, 0, $severity, $file, $line);
});
function check($condition, $message) {
if (!$condition) throw new RuntimeException($message);
}
$disk = parseDiskData("Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/mapper/vg-root 1048576 262144 786432 25% /\n");
check($disk['available_data'] && $disk['size'] === '1 GiB' && $disk['usage'] === '25%', 'Parse POSIX df sizes and usage');
foreach ([false, null, '', 'df: /dev/sdb1: No such file or directory', "Filesystem 1024-blocks Used Available Capacity Mounted on\n"] as $output) {
check(!parseDiskData($output)['available_data'], 'Missing disk output must be unavailable');
}
$healthy = "Personalities : [raid1] [raid5]\nmd0 : active raid5 sda[0] sdb[1] sdc[2]\n 1000 blocks [3/3] [UUU]\n\nunused devices: <none>\n";
check(parseRaidStatus($healthy)['status'] === 'healthy', 'Three-disk RAID5 should be healthy');
check(parseRaidStatus(str_replace('[UUU]', '[U_U]', $healthy))['status'] === 'degraded', 'Missing RAID member should be degraded');
check(parseRaidStatus($healthy . "md1 : active raid1 sdd[0] sde[1]\n 1000 blocks [2/1] [U_]\n")['status'] === 'degraded', 'One failed array must make overall status degraded');
check(parseRaidStatus(str_replace('[UUU]', "[UUU]\n resync = 42.0%", $healthy))['status'] === 'syncing', 'RAID resync is visible');
check(parseRaidStatus("md0 : active raid0 sda[0] sdb[1]\n 1000 blocks super 1.2 512k chunks\n")['status'] === 'active', 'RAID0 does not require a redundancy bitmap');
check(parseRaidStatus("md0 : inactive sda[0](S)\n 1000 blocks\n")['status'] === 'inactive', 'Inactive array remains inactive');
check(parseRaidStatus("Personalities : [raid1]\nunused devices: <none>\n")['status'] === 'not configured', 'No configured Linux MD array');
check(parseRaidStatus(false)['status'] === 'unavailable', 'Unreadable mdstat is unavailable');
$resources = parseSystemStats('97,5', '1024,256,768,0,0,0');
check($resources['cpu']['usage'] === '2.5' && $resources['memory']['usage'] === '25%' && $resources['memory']['swap_usage'] === '0%', 'Resource values and zero swap');
check(!parseSystemStats(false, false)['cpu']['available_data'], 'Missing CPU output must not show 100%');
check(!parseSystemStats('not a number', '')['memory']['available_data'], 'Missing memory output must be unavailable');
// Check remote command construction and path quoting with a harmless stub.
$ssh = new class {
public $commands = [];
function exec($command) { $this->commands[] = $command; return false; }
};
$defaultPath = "/storage/owner's files";
$systemData = getSystemData($ssh, $defaultPath);
check(in_array("LC_ALL=C df -Pk -- '/' 2>/dev/null", $ssh->commands, true), 'System filesystem uses root instead of a device name');
check(in_array("LC_ALL=C df -Pk -- '/storage/owner'\\''s files' 2>/dev/null", $ssh->commands, true), 'Storage path is quoted for remote Linux even on Windows');
// Exercise the actual page template with missing output and with valid output.
$source = file_get_contents(__DIR__ . '/../Web/content/ftp/serverstat.php');
$template = substr($source, strpos($source, '$monitors ='));
foreach ([false, true] as $available) {
if ($available) {
$systemData['storage'] = ['system' => $disk, 'data' => $disk];
$systemData['resources'] = $resources;
$systemData['raid'] = parseRaidStatus($healthy);
}
ob_start();
eval($template);
$html = ob_get_clean();
check(strpos($html, 'Server status') !== false, 'Status page renders');
check($available ? strpos($html, '1 GiB') !== false : strpos($html, 'Storage statistics are unavailable') !== false, 'Template handles available and missing data');
}
echo "Server statistics: parser, RAID, command quoting and page rendering checks passed.\n";
+96
View File
@@ -0,0 +1,96 @@
<?php
require_once __DIR__ . '/../Web/includes/workspace.php';
require_once __DIR__ . '/../Web/includes/auth.php';
require_once __DIR__ . '/../Web/includes/remotepaths.php';
require_once __DIR__ . '/../Web/content/ftp/lib/serverstats.php';
set_error_handler(function ($severity, $message, $file, $line) {
if (error_reporting() & $severity) throw new ErrorException($message, 0, $severity, $file, $line);
});
function expectWorkspace($condition, $message) {
if (!$condition) throw new RuntimeException($message);
}
function renderWorkspaceFixture($name, $admin = true, $defaultPath = '/mnt/raid', $currentPath = '/mnt/raid/Workspace') {
$_SESSION = ['uname' => 'michal', 'admin' => $admin, 'upPer' => true, 'downPer' => true, 'delPer' => true];
$filePath = $currentPath . '/Example.pdf'; $fileName = basename($filePath); $parentPath = $currentPath;
$fileExtension = 'png'; $fileSize = 245760; $mimeType = 'image/png';
$isImage = true; $isVideo = false; $isAudio = false;
$editable = true; $showLineNumbers = true; $content = "{\n \"workspace\": \"USB RAID\",\n \"status\": \"connected\"\n}\n";
$message = ''; $messageType = ''; $shareCreated = false; $shareError = '';
$share = ['expiration' => '2030-01-01']; $downloadAllowed = false; $token = 'test-only';
$items = ['Documents', 'Photos', 'README.md', 'Example.pdf'];
$imageExtensions = ['png']; $videoExtensions = []; $audioExtensions = []; $editableExtensions = ['md']; $archiveExtensions = [];
$sftp = new class {
function is_dir($name) { return strpos($name, '.') === false; }
function stat($path) { return ['size' => 245760]; }
};
$result = new class {
private $rows = [
['uname' => 'michal', 'admin' => true, 'defPath' => '/mnt/raid', 'delPer' => true, 'downPer' => true, 'upPer' => true],
['uname' => 'guest', 'admin' => false, 'defPath' => '/mnt/raid/shared', 'delPer' => false, 'downPer' => true, 'upPer' => false],
];
function fetch_assoc() { return array_shift($this->rows); }
};
$systemData = [
'storage' => array_fill_keys(['system', 'data'], parseDiskData("Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/md0 1048576 262144 786432 25% /mnt/raid\n")),
'raid' => parseRaidStatus("md0 : active raid5 sda[0] sdb[1] sdc[2]\n 1000 blocks [3/3] [UUU]\n"),
'resources' => parseSystemStats('93', '8192,2048,6144,1024,0,1024'), 'lastUpdate' => '2026-10-16 10:00:00',
];
$top = in_array($name, ['adminpanel', 'changepassword'], true);
$source = file_get_contents(__DIR__ . '/../Web/content/' . ($top ? '' : 'ftp/') . $name . '.php');
if ($name === 'serverstat') {
$start = strpos($source, '$monitors =');
eval(substr($source, $start, strpos($source, '?>', $start) - $start));
}
$start = strpos($source, '<!DOCTYPE html>');
$template = substr($source, $start, strpos($source, '</html>', $start) + 7 - $start);
ob_start(); eval('?>' . $template); return ob_get_clean();
}
$preview = in_array('--preview', $argv, true);
$directory = __DIR__ . '/../Web/.layout-preview';
if ($preview && !is_dir($directory)) mkdir($directory);
$pages = ['index', 'serverstat', 'adminpanel', 'changepassword', 'open', 'view', 'pdf', 'shared'];
foreach ($pages as $name) {
$html = renderWorkspaceFixture($name);
$dom = new DOMDocument();
@$dom->loadHTML($html);
$xpath = new DOMXPath($dom);
expectWorkspace($xpath->query('//body')->length === 1, "$name has one body");
expectWorkspace($xpath->query('//main')->length === 1, "$name has one main region");
expectWorkspace($xpath->query('//aside[contains(@class,"sidebar")]')->length === 1, "$name has shared sidebar");
expectWorkspace($xpath->query('//*[@id="themeToggle"]')->length === 1, "$name has one theme switch");
expectWorkspace($xpath->query('//a[@aria-current="page"]')->length === 1, "$name has one active navigation item");
expectWorkspace($xpath->query('//main//footer')->length === 1, "$name footer is inside the main region");
if ($name === 'shared') expectWorkspace(strpos($html, 'User management') === false && strpos($html, 'Account settings') === false, 'Public shared preview does not expose account navigation');
if ($preview) {
$base = $name === 'adminpanel' || $name === 'changepassword' ? '../content/' : '../content/ftp/';
$html = str_replace('<head>', '<head><base href="' . $base . '">', $html);
// Browser previews never request a real remote file or perform mutations.
$image = 'data:image/svg+xml,' . rawurlencode('<svg xmlns="http://www.w3.org/2000/svg" width="800" height="380"><rect width="800" height="380" fill="#1c3053"/><circle cx="400" cy="150" r="70" fill="#2563eb"/><text x="400" y="290" text-anchor="middle" fill="white" font-size="28" font-family="sans-serif">USB RAID / Workspace preview</text></svg>');
$html = preg_replace('/src="(?:view|shared)\.php\?[^"\n]+"/', 'src="' . $image . '"', $html);
$html = str_replace("const url = 'pdf.php?file=%2Fmnt%2Fraid%2FWorkspace%2FExample.pdf&stream=1';", "const url = '/Web/.layout-preview/sample.pdf';", $html);
file_put_contents($directory . '/' . $name . '.html', $html);
}
}
$member = renderWorkspaceFixture('index', false);
expectWorkspace(strpos($member, 'User management') === false && strpos($member, '> Server status</a>') === false, 'Member navigation omits admin pages');
expectWorkspace(strpos($member, 'Account settings') !== false, 'Members can access account settings');
$rootChild = renderWorkspaceFixture('index', true, '/', '/etc');
expectWorkspace(strpos($rootChild, 'href="?path=%2F"') !== false, 'Parent link from a root child uses a POSIX slash');
expectWorkspace(strpos($rootChild, '%5C') === false, 'No Windows separator in remote links');
$root = renderWorkspaceFixture('index', true, '/', '/');
expectWorkspace(strpos($root, 'Parent folder') === false, 'Root has no parent-folder link');
expectWorkspace(strpos($root, 'href="?path=%2FDocuments"') !== false, 'Root children use a single leading slash');
if ($preview) {
// One-page PDF fixture for viewer navigation and fullscreen checks.
$stream = "BT /F1 24 Tf 60 700 Td (USB RAID workspace preview) Tj ET";
$objects = ['<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>', '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>', '<< /Length ' . strlen($stream) . ">>\nstream\n" . $stream . "\nendstream"];
$pdf = "%PDF-1.4\n"; $offsets = [0];
foreach ($objects as $i => $object) { $offsets[] = strlen($pdf); $pdf .= ($i + 1) . " 0 obj\n$object\nendobj\n"; }
$xref = strlen($pdf); $pdf .= "xref\n0 6\n0000000000 65535 f \n";
foreach (array_slice($offsets, 1) as $offset) $pdf .= sprintf("%010d 00000 n \n", $offset);
$pdf .= "trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n$xref\n%%EOF";
file_put_contents($directory . '/sample.pdf', $pdf);
}
echo "Workspace: all eight page templates, navigation roles and shared layout checks passed.\n";