From ae21c3c5d8e7c96101d278b5688c7f40133dece9 Mon Sep 17 00:00:00 2001 From: Troy grunt Date: Fri, 13 Feb 2026 21:59:45 +0100 Subject: [PATCH 1/7] admin update, und upload --- .gitignore | 2 + .vscode/settings.json | 3 + README.md | 62 +++++++++- www/admin.php | 268 ++++++++++++++++++++++++++++++++++++++++-- www/card.php | 40 ++++++- www/files/.gitkeep | 1 + 6 files changed, 358 insertions(+), 18 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 www/files/.gitkeep diff --git a/.gitignore b/.gitignore index 13e46b8..af96255 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ www/secret.php www/_user.php +www/files/* +!www/files/.gitkeep diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..221edb8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "todo-tree.tree.scanMode": "workspace only" +} \ No newline at end of file diff --git a/README.md b/README.md index 07296c5..a86a35d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,60 @@ -# businesscard +# Digitale Visitenkarten (Businesscard) -## TODO -- admin ident werte typen \ No newline at end of file +Dieses Projekt stellt eine einfache Verwaltungsoberfläche für digitale Visitenkarten bereit. Jede Karte wird über eine UUID adressiert, die eindeutig einem Datensatz in der Datenbank zugeordnet ist. Besucher:innen rufen die Karte über card.php?uuid= auf, während Administrator:innen über das Passwort-geschützte Dashboard alle Identitäten, Feldwerte, Dateien und UUID-Token pflegen. + +## Architektur + +* **PHP/Apache**-Weboberfläche in www/ mit Frontend (card.php, download.php) und Backend (admin.php plus _sql.php, _func.php, _user.php). +* **MariaDB** (Docker) hält Tabellen für Identitäten, Felder, UUIDs, Berechtigungen, Dateien und Loginversuche (db/init/001_schema.sql). +* **Docker Compose** startet Webserver und Datenbank; www/ wird in den Apache-Container gemountet, www/files/ spiegelt das Upload-Verzeichnis /var/www/files/. +* **Uploadverzeichnis** www/files/ liegt außerhalb des Document Roots und ist als .gitkeep im Repository abgelegt, damit Hochladungen später von download.php referenziert werden. + +## Installation & erster Start + +1. Repository clonen. +2. www/secret.php vom Beispiel kopieren (secret.php.example) und Datenbankzugang konfigurieren, damit _sql.php sich verbinden kann. +3. mkdir -p www/files (oder md www/files unter Windows) ausführen und darauf achten, dass die .gitkeep-Datei vorhanden ist, damit der Ordner beim Containerstart genutzt wird. +4. docker-compose up --build im Projektverzeichnis ausführen, um Apache und MariaDB zu starten. +5. http://localhost/admin.php öffnen; die Standardanmeldedaten stehen in www/_user.php (admin / password) und sollten sofort geändert werden. + +## Admin-Oberfläche & Workflows + +* Login schützt das Dashboard; nach drei gescheiterten Versuchen wird eine IP für eine Stunde gesperrt (admin_login_attempts). +* Das Dashboard zeigt alle Identitäten sowie UUID-Token. Neue Identitäten erstellt man über ?action=identity_create. +* Jedes Identity kann beliebige Felder (identity_fields) enthalten. Der Typ kann single, multi, file oder url sein. +* Dateien werden pro Identität über das neue Upload-Formular hinzugefügt und landen in www/files/. Der Upload speichert Dateiname, generierten Zwischennamen und MIME-Type in files. +* Wenn ein Feld auf Typ file gesetzt ist, zeigt die Auswahl nur Uploads der aktuellen Identität (per files.identity_id). Die ausgewählte Datei-ID wird im Feldwert gespeichert, damit später nur diese Datei angezeigt oder heruntergeladen wird. +* UUID-Token (access_tokens) verbinden eine Identität mit einer eindeutigen Zeichenfolge, können optional Auslaufdaten und Notizen erhalten und dürfen **nur einmal** existieren. Tokenrechte (token_permissions) bestimmen, welche Feldschlüssel Besucher:innen sehen. + +## Besucheransicht (Visitenkarte) + +* /card.php?uuid=<UUID> lädt nur die Felder, die das Token freigibt, sowie Dateien mit demselben Token oder ohne Tokenbindung. +* Dateifelder erscheinen unterhalb der Felder als Downloadlinks, wenn der Token für die Datei berechtigt ist. +* Das Frontend zeigt Meta-Informationen (Ablaufdatum, Notiz) sowie eine gekürzte UUID. Bestehende Admin-Sessions leiten zurück zum Dashboard, damit Redakteur:innen schnell bearbeiten können. + +## Datenbankstruktur im Überblick + +| Tabelle | Zweck | +| --- | --- | +| identities | Identitätsdatensätze mit Name und Timestamp. | +| identity_fields | Key-Value-Felder (Text, mehrzeilig, Datei, URL) pro Identität. | +| access_tokens | UUID-Token pro Identität mit Ablauf, Notiz und eindeutiger Zeichenfolge. | +| token_permissions | Erlaubte Feldschlüssel pro Token. | +| files | Hochgeladene Dateien mit identity_id, filename, stored_name, mime_type und optional token_id. | +| admin_login_attempts | Brute-Force-Schutz mit Zählern und Sperrzeit pro IP. | + +## Sicherheitshinweise + +* **UUIDs sind einmalige Zugriffstoken** und sollten nicht doppelt vergeben oder offen weitergegeben werden, wenn geschützte Daten hinterlegt sind. +* **Admin-Zugang**: Bitte www/_user.php durch eigene Credentials ersetzen oder ein robusteres Auth-Verfahren implementieren. +* **Loginversuche** werden nach drei Fehlversuchen eine Stunde gesperrt (registerFailedLogin, isIpLocked, clearLoginAttempts). +* **Datei-Uploads** liegen im Verzeichnis www/files/ (Container: /var/www/files/) und dürfen ausschließlich über download.php mit gültigem Token ausgeliefert werden. Der Ordner sollte nicht direkt vom Webserver referenziert werden. + +## Weiterentwicklung & Pflege + +1. **Stil & Branding**: Die Inline-CSS im card.php-Head kann durch ein eigenes Template ersetzt oder ausgelagert werden. +2. **Dateien & Automatisierung**: UUIDs und zugehörige Dateien lassen sich per API oder QR-Code verteilen. Die Download-Logik kann um Freigabezeitfenster erweitert werden. +3. **Tests & Validierung**: Neue Felder brauchen eventuell Frontend-Validierung; Dateiuploads sollten auf MIME-Type, Größe und erlaubte Endungen geprüft werden. +4. **Deployment**: Docker Compose genügt lokal. In Produktion empfiehlt sich ein Reverse Proxy, HTTPS und regelmäßige Backups des MariaDB-Volumes (db_data). Auch www/files/ sollte gesichert werden, da dort alle Uploads gespeichert werden. + +> Hinweis: UUIDs repräsentieren digitale Visitenkarten und sind **einmalig**. Jeder Besuch über eine UUID dokumentiert genau eine Karte, deren Sichtbarkeit über Tokenrechte und Datei-Zuordnungen kontrolliert wird. diff --git a/www/admin.php b/www/admin.php index 5a826cc..8d246f7 100644 --- a/www/admin.php +++ b/www/admin.php @@ -81,6 +81,7 @@ button {
+ $attrVal) { + $extraAttrString .= ' ' . htmlspecialchars($attr) . '="' . htmlspecialchars($attrVal) . '"'; + } + + if ($type === 'file') { + $options = ''; + foreach ($filesForIdentity as $file) { + $fileId = (string)(int)$file['id']; + $selected = $fileId === $value ? ' selected' : ''; + $options .= sprintf( + '', + htmlspecialchars($fileId), + $selected, + htmlspecialchars($file['filename']) + ); + } + return ""; + } + + if ($type === 'multi') { + return ""; + } + + $inputType = $type === 'url' ? 'url' : 'text'; + return ""; + } $id = (int)($_GET['id'] ?? 0); $identity = $sql->single( @@ -234,8 +265,94 @@ if ($action === 'identity_edit') { exit('Identität nicht gefunden'); } + $fileUploadErrors = []; + $fileUploadSuccess = 0; + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (isset($_POST['upload_files'])) { + $filesInput = $_FILES['files'] ?? null; + $hasSelection = false; + if ($filesInput) { + if (is_array($filesInput['name'])) { + foreach ($filesInput['name'] as $fileName) { + if (trim((string)$fileName) !== '') { + $hasSelection = true; + break; + } + } + } else { + $hasSelection = trim((string)$filesInput['name']) !== ''; + } + } + + if ($filesInput && $hasSelection) { + $uploadDir = __DIR__ . '/../files/'; + if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true) && !is_dir($uploadDir)) { + $fileUploadErrors[] = 'Upload-Verzeichnis kann nicht erstellt werden.'; + } else { + $total = is_array($filesInput['name']) ? count($filesInput['name']) : 1; + $uploaded = 0; + $finfo = finfo_open(FILEINFO_MIME_TYPE); + for ($i = 0; $i < $total; $i++) { + $originalName = is_array($filesInput['name']) ? $filesInput['name'][$i] : $filesInput['name']; + $error = is_array($filesInput['error']) ? $filesInput['error'][$i] : $filesInput['error']; + $tmpName = is_array($filesInput['tmp_name']) ? $filesInput['tmp_name'][$i] : $filesInput['tmp_name']; + + if ($error === UPLOAD_ERR_NO_FILE) { + continue; + } + + $originalName = trim((string)$originalName); + if ($originalName === '') { + continue; + } + + if ($error !== UPLOAD_ERR_OK) { + $fileUploadErrors[] = sprintf('Fehler beim Hochladen von %s.', $originalName); + continue; + } + + if (!is_uploaded_file($tmpName)) { + $fileUploadErrors[] = sprintf('Datei %s konnte nicht verarbeitet werden.', $originalName); + continue; + } + + $safeName = preg_replace('/[^A-Za-z0-9._-]/', '_', basename($originalName)); + if ($safeName === '') { + $safeName = 'file'; + } + $storedName = bin2hex(random_bytes(12)) . '_' . $safeName; + $destination = $uploadDir . $storedName; + + if (!move_uploaded_file($tmpName, $destination)) { + $fileUploadErrors[] = sprintf('Speichern von %s fehlgeschlagen.', $originalName); + continue; + } + + $mimeType = $finfo ? finfo_file($finfo, $destination) : 'application/octet-stream'; + $sql->set( + "INSERT INTO files (identity_id, filename, stored_name, mime_type) + VALUES (?, ?, ?, ?)", + "isss", + [$id, $originalName, $storedName, $mimeType] + ); + $uploaded++; + } + + if ($finfo) { + finfo_close($finfo); + } + + if ($uploaded > 0) { + $fileUploadSuccess = $uploaded; + } + } + } else { + $fileUploadErrors[] = 'Bitte wählen Sie mindestens eine Datei aus.'; + } + } + // Identität umbenennen if (isset($_POST['rename'])) { $sql->set( @@ -296,6 +413,15 @@ if ($action === 'identity_edit') { "i", [$id] ); + + $identityFiles = $sql->get( + "SELECT id, filename FROM files WHERE identity_id = ? ORDER BY uploaded_at DESC", + "i", + [$id] + ); + if ($identityFiles === false) { + $identityFiles = []; + } ?> @@ -334,17 +460,13 @@ if ($action === 'identity_edit') { - - - - - - + +
+ +
- @@ -367,8 +489,10 @@ if ($action === 'identity_edit') {

Neues Feld

- - @@ -377,6 +501,125 @@ if ($action === 'identity_edit') {
+

Dateien hochladen

+
+ + +
+ + 0): ?> +

+ Datei hochgeladen. +

+ + + + + + + +

Vorhandene Dateien

+ + + + + +

← zurück

@@ -409,7 +652,6 @@ if ($action === 'uuid_create') { EDIT UUID ───────────────────────────── */ if ($action === 'uuid_edit') { - //TODO es fehlt die auswahl welche felder sichtbar sein sollen. $uuid = $_GET['uuid'] ?? ''; $token = $sql->single( diff --git a/www/card.php b/www/card.php index 48562c2..371936b 100644 --- a/www/card.php +++ b/www/card.php @@ -170,6 +170,30 @@ function label(string $key): string { text-align: center; font-size: .75rem; color: var(--muted); + display: flex; + flex-direction: column; + gap: .25rem; + align-items: center; + } + + .footer-meta { + display: flex; + gap: .65rem; + font-size: .7rem; + flex-wrap: wrap; + justify-content: center; + color: var(--muted); + } + + .footer-meta span { + padding: .15rem .4rem; + border-radius: 999px; + background: rgba(148, 163, 184, .18); + } + + .footer-note { + font-size: .65rem; + color: rgba(229, 231, 235, .7); } @@ -203,10 +227,22 @@ function label(string $key): string {
- Zugriff über sicheren Link + +
diff --git a/www/files/.gitkeep b/www/files/.gitkeep new file mode 100644 index 0000000..cd4950f --- /dev/null +++ b/www/files/.gitkeep @@ -0,0 +1 @@ +This directory stores uploaded files outside the document root. From 376bfdeafe7a1e9884eec061666346407a10e3fb Mon Sep 17 00:00:00 2001 From: Troy grunt Date: Fri, 13 Feb 2026 22:11:55 +0100 Subject: [PATCH 2/7] upload nach _files --- .gitignore | 4 ++-- README.md | 12 ++++++------ www/admin.php | 2 +- www/download.php | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index af96255..ce93d1d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ www/secret.php www/_user.php -www/files/* -!www/files/.gitkeep +www/_files/* +!www/_files/.gitkeep diff --git a/README.md b/README.md index a86a35d..747fb2c 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ Dieses Projekt stellt eine einfache Verwaltungsoberfläche für digitale Visiten * **PHP/Apache**-Weboberfläche in www/ mit Frontend (card.php, download.php) und Backend (admin.php plus _sql.php, _func.php, _user.php). * **MariaDB** (Docker) hält Tabellen für Identitäten, Felder, UUIDs, Berechtigungen, Dateien und Loginversuche (db/init/001_schema.sql). -* **Docker Compose** startet Webserver und Datenbank; www/ wird in den Apache-Container gemountet, www/files/ spiegelt das Upload-Verzeichnis /var/www/files/. -* **Uploadverzeichnis** www/files/ liegt außerhalb des Document Roots und ist als .gitkeep im Repository abgelegt, damit Hochladungen später von download.php referenziert werden. +* **Docker Compose** startet Webserver und Datenbank; www/ wird in den Apache-Container gemountet, www/_files/ spiegelt das Upload-Verzeichnis /var/www/html/_files/. +* **Uploadverzeichnis** www/_files/ liegt außerhalb des Document Roots und ist als .gitkeep im Repository abgelegt, damit Hochladungen später von download.php referenziert werden. ## Installation & erster Start 1. Repository clonen. 2. www/secret.php vom Beispiel kopieren (secret.php.example) und Datenbankzugang konfigurieren, damit _sql.php sich verbinden kann. -3. mkdir -p www/files (oder md www/files unter Windows) ausführen und darauf achten, dass die .gitkeep-Datei vorhanden ist, damit der Ordner beim Containerstart genutzt wird. +3. mkdir -p www/_files (oder md www/_files unter Windows) ausführen und darauf achten, dass die .gitkeep-Datei vorhanden ist, damit der Ordner beim Containerstart genutzt wird. 4. docker-compose up --build im Projektverzeichnis ausführen, um Apache und MariaDB zu starten. 5. http://localhost/admin.php öffnen; die Standardanmeldedaten stehen in www/_user.php (admin / password) und sollten sofort geändert werden. @@ -22,7 +22,7 @@ Dieses Projekt stellt eine einfache Verwaltungsoberfläche für digitale Visiten * Login schützt das Dashboard; nach drei gescheiterten Versuchen wird eine IP für eine Stunde gesperrt (admin_login_attempts). * Das Dashboard zeigt alle Identitäten sowie UUID-Token. Neue Identitäten erstellt man über ?action=identity_create. * Jedes Identity kann beliebige Felder (identity_fields) enthalten. Der Typ kann single, multi, file oder url sein. -* Dateien werden pro Identität über das neue Upload-Formular hinzugefügt und landen in www/files/. Der Upload speichert Dateiname, generierten Zwischennamen und MIME-Type in files. +* Dateien werden pro Identität über das neue Upload-Formular hinzugefügt und landen in www/_files/. Der Upload speichert Dateiname, generierten Zwischennamen und MIME-Type in files. * Wenn ein Feld auf Typ file gesetzt ist, zeigt die Auswahl nur Uploads der aktuellen Identität (per files.identity_id). Die ausgewählte Datei-ID wird im Feldwert gespeichert, damit später nur diese Datei angezeigt oder heruntergeladen wird. * UUID-Token (access_tokens) verbinden eine Identität mit einer eindeutigen Zeichenfolge, können optional Auslaufdaten und Notizen erhalten und dürfen **nur einmal** existieren. Tokenrechte (token_permissions) bestimmen, welche Feldschlüssel Besucher:innen sehen. @@ -48,13 +48,13 @@ Dieses Projekt stellt eine einfache Verwaltungsoberfläche für digitale Visiten * **UUIDs sind einmalige Zugriffstoken** und sollten nicht doppelt vergeben oder offen weitergegeben werden, wenn geschützte Daten hinterlegt sind. * **Admin-Zugang**: Bitte www/_user.php durch eigene Credentials ersetzen oder ein robusteres Auth-Verfahren implementieren. * **Loginversuche** werden nach drei Fehlversuchen eine Stunde gesperrt (registerFailedLogin, isIpLocked, clearLoginAttempts). -* **Datei-Uploads** liegen im Verzeichnis www/files/ (Container: /var/www/files/) und dürfen ausschließlich über download.php mit gültigem Token ausgeliefert werden. Der Ordner sollte nicht direkt vom Webserver referenziert werden. +* **Datei-Uploads** liegen im Verzeichnis www/_files/ (Container: /var/www/html/_files/) und dürfen ausschließlich über download.php mit gültigem Token ausgeliefert werden. Der Ordner sollte nicht direkt vom Webserver referenziert werden. ## Weiterentwicklung & Pflege 1. **Stil & Branding**: Die Inline-CSS im card.php-Head kann durch ein eigenes Template ersetzt oder ausgelagert werden. 2. **Dateien & Automatisierung**: UUIDs und zugehörige Dateien lassen sich per API oder QR-Code verteilen. Die Download-Logik kann um Freigabezeitfenster erweitert werden. 3. **Tests & Validierung**: Neue Felder brauchen eventuell Frontend-Validierung; Dateiuploads sollten auf MIME-Type, Größe und erlaubte Endungen geprüft werden. -4. **Deployment**: Docker Compose genügt lokal. In Produktion empfiehlt sich ein Reverse Proxy, HTTPS und regelmäßige Backups des MariaDB-Volumes (db_data). Auch www/files/ sollte gesichert werden, da dort alle Uploads gespeichert werden. +4. **Deployment**: Docker Compose genügt lokal. In Produktion empfiehlt sich ein Reverse Proxy, HTTPS und regelmäßige Backups des MariaDB-Volumes (db_data). Auch www/_files/ sollte gesichert werden, da dort alle Uploads gespeichert werden. > Hinweis: UUIDs repräsentieren digitale Visitenkarten und sind **einmalig**. Jeder Besuch über eine UUID dokumentiert genau eine Karte, deren Sichtbarkeit über Tokenrechte und Datei-Zuordnungen kontrolliert wird. diff --git a/www/admin.php b/www/admin.php index 8d246f7..a248594 100644 --- a/www/admin.php +++ b/www/admin.php @@ -287,7 +287,7 @@ if ($action === 'identity_edit') { } if ($filesInput && $hasSelection) { - $uploadDir = __DIR__ . '/../files/'; + $uploadDir = __DIR__ . '/_files/'; if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true) && !is_dir($uploadDir)) { $fileUploadErrors[] = 'Upload-Verzeichnis kann nicht erstellt werden.'; } else { diff --git a/www/download.php b/www/download.php index 988f7b8..5009777 100644 --- a/www/download.php +++ b/www/download.php @@ -51,10 +51,10 @@ if (!$file) { /** * Dateipfad */ -$basePath = '/var/www/files/'; -$path = realpath($basePath . $file['stored_name']); +$basePath = realpath(__DIR__ . '/_files'); +$path = $basePath ? realpath($basePath . '/' . $file['stored_name']) : null; -if (!$path || !str_starts_with($path, $basePath) || !is_file($path)) { +if (!$path || !$basePath || !str_starts_with($path, $basePath) || !is_file($path)) { http_response_code(404); exit('Datei fehlt'); } From b07a2ee24d93f5271eaad12c75291c163a0f5341 Mon Sep 17 00:00:00 2001 From: Troy grunt Date: Fri, 13 Feb 2026 22:49:06 +0100 Subject: [PATCH 3/7] TODOs --- README.md | 2 +- docker-compose.yml | 1 + www/admin.php | 85 +++++++++++++++++++++++++++++++++++++++++++--- www/files/.gitkeep | 1 - 4 files changed, 83 insertions(+), 6 deletions(-) delete mode 100644 www/files/.gitkeep diff --git a/README.md b/README.md index 747fb2c..726d148 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Dieses Projekt stellt eine einfache Verwaltungsoberfläche für digitale Visiten * Login schützt das Dashboard; nach drei gescheiterten Versuchen wird eine IP für eine Stunde gesperrt (admin_login_attempts). * Das Dashboard zeigt alle Identitäten sowie UUID-Token. Neue Identitäten erstellt man über ?action=identity_create. * Jedes Identity kann beliebige Felder (identity_fields) enthalten. Der Typ kann single, multi, file oder url sein. -* Dateien werden pro Identität über das neue Upload-Formular hinzugefügt und landen in www/_files/. Der Upload speichert Dateiname, generierten Zwischennamen und MIME-Type in files. +* Dateien werden pro Identität über das neue Upload-Formular hinzugefügt und landen in www/_files/. Der Upload speichert Dateiname, generierten Zwischennamen und MIME-Type in files. Unter dem Formular zeigt das Admin-UI eine Liste der bereits hochgeladenen Dateien inkl. einer Löschoption. * Wenn ein Feld auf Typ file gesetzt ist, zeigt die Auswahl nur Uploads der aktuellen Identität (per files.identity_id). Die ausgewählte Datei-ID wird im Feldwert gespeichert, damit später nur diese Datei angezeigt oder heruntergeladen wird. * UUID-Token (access_tokens) verbinden eine Identität mit einer eindeutigen Zeichenfolge, können optional Auslaufdaten und Notizen erhalten und dürfen **nur einmal** existieren. Tokenrechte (token_permissions) bestimmen, welche Feldschlüssel Besucher:innen sehen. diff --git a/docker-compose.yml b/docker-compose.yml index 1af2243..0b4ed12 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - ./www:/var/www/html depends_on: - db + command: ["sh", "-c", "mkdir -p /var/www/html/_files && chown -R www-data:www-data /var/www/html/_files && apache2-foreground"] db: image: mariadb:11 diff --git a/www/admin.php b/www/admin.php index a248594..b7ed9c5 100644 --- a/www/admin.php +++ b/www/admin.php @@ -265,10 +265,30 @@ if ($action === 'identity_edit') { exit('Identität nicht gefunden'); } - $fileUploadErrors = []; - $fileUploadSuccess = 0; + $uploadErrorLabel = function (int $error): string { + return match ($error) { + UPLOAD_ERR_INI_SIZE => 'Datei überschreitet upload_max_filesize', + UPLOAD_ERR_FORM_SIZE => 'Datei überschreitet MAX_FILE_SIZE', + UPLOAD_ERR_PARTIAL => 'Datei wurde nur teilweise hochgeladen', + UPLOAD_ERR_NO_FILE => 'keine Datei ausgewählt', + UPLOAD_ERR_NO_TMP_DIR => 'temporäres Verzeichnis fehlt', + UPLOAD_ERR_CANT_WRITE => 'Datei konnte nicht geschrieben werden', + UPLOAD_ERR_EXTENSION => 'Upload durch eine Erweiterung abgebrochen', + default => 'unbekannter Upload-Fehler', + }; + }; + + $fileUploadErrors = $_SESSION['fileUploadErrors'] ?? []; + unset($_SESSION['fileUploadErrors']); + $fileUploadSuccess = (int)($_SESSION['fileUploadSuccess'] ?? 0); + unset($_SESSION['fileUploadSuccess']); + $fileDeleteMessage = $_SESSION['fileDeleteMessage'] ?? ''; + unset($_SESSION['fileDeleteMessage']); if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $fileUploadErrors = []; + $fileUploadSuccess = 0; + $fileDeleteMessage = ''; if (isset($_POST['upload_files'])) { $filesInput = $_FILES['files'] ?? null; @@ -309,7 +329,11 @@ if ($action === 'identity_edit') { } if ($error !== UPLOAD_ERR_OK) { - $fileUploadErrors[] = sprintf('Fehler beim Hochladen von %s.', $originalName); + $fileUploadErrors[] = sprintf( + 'Fehler beim Hochladen von %s: %s.', + $originalName, + $uploadErrorLabel($error) + ); continue; } @@ -353,6 +377,33 @@ if ($action === 'identity_edit') { } } + if (isset($_POST['delete_file'])) { + $fileId = (int)($_POST['file_id'] ?? 0); + if ($fileId > 0) { + $file = $sql->single( + "SELECT stored_name, filename FROM files WHERE id = ? AND identity_id = ?", + "ii", + [$fileId, $id] + ); + if ($file) { + $diskPath = __DIR__ . '/_files/' . $file['stored_name']; + if (is_file($diskPath)) { + @unlink($diskPath); + } + $sql->set( + "DELETE FROM files WHERE id = ?", + "i", + [$fileId] + ); + $fileDeleteMessage = sprintf('Datei "%s" gelöscht.', $file['filename']); + } else { + $fileUploadErrors[] = 'Datei nicht gefunden oder gehört nicht zu dieser Identität.'; + } + } else { + $fileUploadErrors[] = 'Ungültige Datei.'; + } + } + // Identität umbenennen if (isset($_POST['rename'])) { $sql->set( @@ -404,6 +455,10 @@ if ($action === 'identity_edit') { ); } + $_SESSION['fileUploadErrors'] = $fileUploadErrors; + $_SESSION['fileUploadSuccess'] = $fileUploadSuccess; + $_SESSION['fileDeleteMessage'] = $fileDeleteMessage; + header("Location: admin.php?action=identity_edit&id=$id"); exit; } @@ -523,9 +578,19 @@ if ($action === 'identity_edit') {

Vorhandene Dateien

+ +

+
    -
  • (ID )
  • +
  • + (ID ) +
    + + +
    +
@@ -806,4 +871,16 @@ $identities = $sql->get("SELECT * FROM identities ORDER BY id DESC"); diff --git a/www/files/.gitkeep b/www/files/.gitkeep deleted file mode 100644 index cd4950f..0000000 --- a/www/files/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -This directory stores uploaded files outside the document root. From a730b30d4dd37cc234135cdd701226d54d76caf7 Mon Sep 17 00:00:00 2001 From: Troy grunt Date: Fri, 13 Feb 2026 22:57:23 +0100 Subject: [PATCH 4/7] uploade size --- www/admin.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/www/admin.php b/www/admin.php index b7ed9c5..b7924f9 100644 --- a/www/admin.php +++ b/www/admin.php @@ -6,6 +6,11 @@ require '_user.php'; session_start(); $ip = $_SERVER['REMOTE_ADDR']; +const MAX_UPLOAD_FILE_SIZE_MB = 32; +const MAX_UPLOAD_FILE_SIZE_BYTES = MAX_UPLOAD_FILE_SIZE_MB * 1024 * 1024; +ini_set('upload_max_filesize', MAX_UPLOAD_FILE_SIZE_MB . 'M'); +ini_set('post_max_size', (MAX_UPLOAD_FILE_SIZE_MB + 1) . 'M'); + /* ───────────────────────────── Security ───────────────────────────── */ @@ -872,8 +877,6 @@ $identities = $sql->get("SELECT * FROM identities ORDER BY id DESC"); Date: Fri, 13 Feb 2026 23:05:18 +0100 Subject: [PATCH 5/7] =?UTF-8?q?download=20nur=20mit=20g=C3=BCltiger=20uui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- www/admin.php | 1 - www/download.php | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/www/admin.php b/www/admin.php index b7924f9..a8d01e4 100644 --- a/www/admin.php +++ b/www/admin.php @@ -885,5 +885,4 @@ $identities = $sql->get("SELECT * FROM identities ORDER BY id DESC"); //TODO anzeige von einer gelöschten oder leeren datei ausblenden, bei vollzogenem löschvorgang -//TODO prüfen ob dateien nur mit korrektem uuid herunterladbar sind ?> diff --git a/www/download.php b/www/download.php index 5009777..b841e47 100644 --- a/www/download.php +++ b/www/download.php @@ -6,13 +6,19 @@ require '_func.php'; * Eingaben prüfen */ $fileId = isset($_GET['id']) ? (int)$_GET['id'] : 0; -$uuid = $_GET['uuid'] ?? null; +$uuid = trim($_GET['uuid'] ?? ''); if ($fileId <= 0 || !$uuid) { http_response_code(400); exit('Ungültige Anfrage'); } +if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $uuid)) { + http_response_code(400); + exit('Ungültige Anfrage'); + //TODO fehlgeschlagener zugriff als fehlerhafte anmeldung ansehen und bei wiederholung ip sperren +} + /** * Token validieren */ From 42541f56037d40ee81e5b7318107a0d60fcd1562 Mon Sep 17 00:00:00 2001 From: Troy grunt Date: Fri, 13 Feb 2026 23:12:15 +0100 Subject: [PATCH 6/7] =?UTF-8?q?pr=C3=BCfen=20was=20passiert,=20wenn=20mehr?= =?UTF-8?q?ere=20dateien=20mit=20dem=20gleichen=20dateinamen=20hochgeladen?= =?UTF-8?q?=20werden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- www/admin.php | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/www/admin.php b/www/admin.php index a8d01e4..5446a96 100644 --- a/www/admin.php +++ b/www/admin.php @@ -351,8 +351,10 @@ if ($action === 'identity_edit') { if ($safeName === '') { $safeName = 'file'; } - $storedName = bin2hex(random_bytes(12)) . '_' . $safeName; - $destination = $uploadDir . $storedName; + do { + $storedName = bin2hex(random_bytes(12)) . '_' . $safeName; + $destination = $uploadDir . $storedName; + } while (is_file($destination)); if (!move_uploaded_file($tmpName, $destination)) { $fileUploadErrors[] = sprintf('Speichern von %s fehlgeschlagen.', $originalName); @@ -482,6 +484,18 @@ if ($action === 'identity_edit') { if ($identityFiles === false) { $identityFiles = []; } + $duplicateFilenameCounts = []; + foreach ($identityFiles as $file) { + $filename = $file['filename'] ?? ''; + if ($filename === '') { + continue; + } + $duplicateFilenameCounts[$filename] = ($duplicateFilenameCounts[$filename] ?? 0) + 1; + } + $duplicateFileNames = array_keys(array_filter( + $duplicateFilenameCounts, + static fn (int $count): bool => $count > 1 + )); ?> @@ -586,10 +600,18 @@ if ($action === 'identity_edit') {

+ +

+ Dateien mit identischem Dateinamen bleiben getrennt und können einzeln gelöscht. +

+
  • (ID ) + + mehrfach vorhanden +
    + +

    Dateien

    + + +

    ← zurück

    get("SELECT * FROM identities ORDER BY id DESC");