From 4e3ff6447fd97b50685ca5f2549a36f758e62f3f Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 15 Jul 2026 13:59:54 +0200 Subject: [PATCH] Foto-Backend /foto-upload: alle Schulbuch-Foto-Slots mit Upload-Formular MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Parst buchBild()-Slots live aus allen buch-*.php (92 Slots, bleibt automatisch synchron) - Pro Slot: Beschreibung, Status (eingebaut/wartend/gesucht), Thumbnail, Multi-Upload (JPG/PNG/HEIC/WebP, max 40 MB) + Notizfeld; Sonstiges-Slot für freie Fotos - Uploads nach uploads/buch-fotos//, per .htaccess gesperrt (Originale mit GPS nie öffentlich); PRG-Redirect, Dateityp-/Größen-Validierung - App/uploads/ in .gitignore Co-Authored-By: Claude Fable 5 --- .gitignore | 1 + App/pages/foto-upload.php | 227 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 App/pages/foto-upload.php diff --git a/.gitignore b/.gitignore index ca51de8..e2bb057 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ secret_keys.txt **/.humaninput/ **/.env !**/.env.example +App/uploads/ diff --git a/App/pages/foto-upload.php b/App/pages/foto-upload.php new file mode 100644 index 0000000..44b4389 --- /dev/null +++ b/App/pages/foto-upload.php @@ -0,0 +1,227 @@ +/ — dieser + * Ordner ist per .htaccess gesperrt (Originale enthalten EXIF/GPS!). + * Atlas holt sie vom Server, strippt EXIF, skaliert und baut sie in die Slots ein. + */ +require_once __DIR__ . '/../php/config/app.php'; +$bp = BASE_PATH; + +$UPLOAD_BASE = __DIR__ . '/../uploads/buch-fotos'; +$MAX_BYTES = 40 * 1024 * 1024; // 40 MB je Datei +$MAX_FILES = 12; // je Absenden +$ALLOWED_EXT = ['jpg','jpeg','png','heic','heif','webp','tif','tiff']; + +/* ---------- Slots aus den Kapiteln parsen ---------- */ +$ORDER = ['tourismustal','tourismusregion','kofferdetektiv','busfahrt','logistik', + 'weltkueche','farmer','klima','fluss','energiemanager','heli', + 'sonnensystem','eu-werkstatt']; +$KAPITEL = []; +foreach ($ORDER as $id) { + $file = __DIR__ . "/buch-{$id}.php"; + if (!is_file($file)) continue; + $src = file_get_contents($file); + $titel = preg_match('/

(.*?)<\/h1>/s', $src, $m) ? trim(strip_tags($m[1])) : $id; + preg_match_all("/buchBild\(\s*'((?:[^'\\\\]|\\\\.)*)'\s*,\s*'((?:[^'\\\\]|\\\\.)*)'/s", $src, $mm, PREG_SET_ORDER); + $slots = []; + foreach ($mm as $b) { + $slug = stripcslashes($b[1]); + $vorschlag = stripcslashes($b[2]); + // Leicht- und Normal-Version rufen denselben Slot auf → längste Beschreibung gewinnt + if (!isset($slots[$slug]) || mb_strlen($vorschlag) > mb_strlen($slots[$slug])) { + $slots[$slug] = $vorschlag; + } + } + if ($slots) $KAPITEL[$id] = ['titel' => $titel, 'slots' => $slots]; +} + +/* ---------- Helfer ---------- */ +function slotBildPfad(string $sim, string $slug): ?string { + foreach (['jpg','jpeg','webp','png'] as $ext) { + $rel = "assets/img/buch/{$sim}/{$slug}.{$ext}"; + if (is_file(__DIR__ . '/../' . $rel)) return $rel; + } + return null; +} +function pendingUploads(string $base, string $sim, string $slug): array { + $dir = "{$base}/{$sim}"; + if (!is_dir($dir)) return []; + $out = []; + foreach (glob($dir . '/' . $slug . '__*') as $f) { + if (substr($f, -9) === '.note.txt') continue; + $out[] = ['name' => basename($f), 'kb' => (int)round(filesize($f) / 1024)]; + } + return $out; +} +function safeName(string $s): string { + $s = preg_replace('/[^A-Za-z0-9._-]+/', '-', $s); + return trim(mb_substr($s, 0, 80), '-.') ?: 'foto'; +} + +/* ---------- Upload verarbeiten (PRG-Muster) ---------- */ +$fehler = []; +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $sim = $_POST['sim'] ?? ''; + $slug = preg_replace('/[^a-z0-9-]/', '', $_POST['slug'] ?? ''); + $note = trim($_POST['note'] ?? ''); + + $gueltig = ($sim === 'sonstiges' && $slug !== '') + || (isset($KAPITEL[$sim]) && isset($KAPITEL[$sim]['slots'][$slug])); + if (!$gueltig) { + $fehler[] = 'Unbekannter Foto-Slot.'; + } else { + $dir = "{$UPLOAD_BASE}/{$sim}"; + if (!is_dir($dir)) mkdir($dir, 0775, true); + // Ordner absichern: Originale (mit GPS!) dürfen nie öffentlich abrufbar sein + if (!is_file($UPLOAD_BASE . '/.htaccess')) { + file_put_contents($UPLOAD_BASE . '/.htaccess', "Require all denied\n"); + } + $ts = date('Ymd-His'); + $gespeichert = 0; + if (!empty($_FILES['fotos']) && is_array($_FILES['fotos']['name'])) { + $n = min(count($_FILES['fotos']['name']), $MAX_FILES); + for ($i = 0; $i < $n; $i++) { + if ($_FILES['fotos']['error'][$i] === UPLOAD_ERR_NO_FILE) continue; + if ($_FILES['fotos']['error'][$i] !== UPLOAD_ERR_OK) { $fehler[] = 'Upload-Fehler bei Datei ' . ($i + 1) . ' (Code ' . $_FILES['fotos']['error'][$i] . ').'; continue; } + if ($_FILES['fotos']['size'][$i] > $MAX_BYTES) { $fehler[] = htmlspecialchars($_FILES['fotos']['name'][$i]) . ' ist größer als 40 MB.'; continue; } + $orig = $_FILES['fotos']['name'][$i]; + $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION)); + if (!in_array($ext, $ALLOWED_EXT, true)) { $fehler[] = htmlspecialchars($orig) . ': Dateityp .' . htmlspecialchars($ext) . ' nicht erlaubt.'; continue; } + $ziel = "{$dir}/{$slug}__{$ts}__" . ($i + 1) . '__' . safeName($orig); + if (move_uploaded_file($_FILES['fotos']['tmp_name'][$i], $ziel)) $gespeichert++; + else $fehler[] = htmlspecialchars($orig) . ' konnte nicht gespeichert werden.'; + } + } + if ($gespeichert === 0 && !$fehler) $fehler[] = 'Keine Datei ausgewählt.'; + if ($note !== '' && ($gespeichert > 0 || $sim === 'sonstiges')) { + file_put_contents("{$dir}/{$slug}__{$ts}.note.txt", $note . "\n"); + } + if ($gespeichert > 0 && !$fehler) { + header("Location: ?ok={$gespeichert}&sim=" . urlencode($sim) . "&slug=" . urlencode($slug) . "#{$sim}-{$slug}"); + exit; + } + } +} +$ok = isset($_GET['ok']) ? (int)$_GET['ok'] : 0; +$okAnker = $ok ? (($_GET['sim'] ?? '') . '-' . ($_GET['slug'] ?? '')) : ''; + +/* Übersichtszahlen */ +$gesamt = 0; $mitFoto = 0; $wartend = 0; +foreach ($KAPITEL as $sim => $k) { + foreach ($k['slots'] as $slug => $v) { + $gesamt++; + if (slotBildPfad($sim, $slug)) $mitFoto++; + if (pendingUploads($UPLOAD_BASE, $sim, $slug)) $wartend++; + } +} +?> + + + + + +📷 Foto-Backend · Schulbuch · GeoGraSim + + + +
+

📷 Foto-Backend zum Schulbuch

+

Alle Foto-Slots aus den 13 Schulbuch-Kapiteln mit ihren + Foto-Wünschen. Zu jedem Slot kannst du direkt Fotos hochladen (mehrere auf einmal, auch HEIC + vom iPhone/iPad) und eine Notiz dazuschreiben — Atlas holt sie ab, entfernt die GPS-Daten, + verkleinert sie und baut sie ein.

+ 📷 Slots + eingebaut + 📥 mit wartenden Uploads + +
🔒 Hochgeladene Original-Dateien sind nicht öffentlich abrufbar + (Ordner gesperrt) — deine GPS-Daten bleiben privat. Veröffentlicht wird nur die bereinigte + Version im Kapitel.
+ +
Foto 1 ? 's' : '' ?> hochgeladen — danke! Atlas kümmert sich um Zuschnitt und Einbau.
+
⚠️
+ +
Kapitel: + $k): ?> + · + + Sonstiges +
+ + $k): ?> +

+ $vorschlag): + $bild = slotBildPfad($sim, $slug); + $pend = pendingUploads($UPLOAD_BASE, $sim, $slug); + $status = $bild ? 'hat' : ($pend ? 'wartet' : 'offen'); + ?> +
+
+ + +
+

+ +
📥 ( KB)
+
+ + + + + +
+
+ + + +

Sonstiges / ohne Zuordnung

+
+

Fotos, die zu keinem Slot passen, aber gut sein könnten — mit kurzer + Beschreibung, wofür du sie siehst. Atlas ordnet sie zu oder legt neue Slots an.

+
+ + + + + +
+
+
+ +