/ — 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; /* ---------- Zugang: eingeloggte:r Admin ODER PIN (Ausnahme, vorläufig) ---------- */ require_once __DIR__ . '/../php/lib/Session.php'; $__isAdmin = (bool)Session::adminId(); /* ---------- PIN-Schranke (im Browser gespeichert, 1 Jahr) ---------- */ $PIN = '0815'; $pinFehler = false; if (isset($_POST['pin'])) { if ($_POST['pin'] === $PIN) { setcookie('ggs_foto_pin', $PIN, time() + 60 * 60 * 24 * 365, '/'); header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?')); exit; } $pinFehler = true; } if (!$__isAdmin && ($_COOKIE['ggs_foto_pin'] ?? '') !== $PIN) { ?> 📷 Foto-Backend · PIN

📷 Foto-Backend

Bitte PIN eingeben — dein Browser merkt sie sich.

PIN stimmt nicht.
'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', 'heic' => 'image/heic', 'heif' => 'image/heif', 'tif' => 'image/tiff', 'tiff' => 'image/tiff'][$ext] ?? 'application/octet-stream'; if (isset($_GET['thumb'])) { $tdir = "{$UPLOAD_BASE}/.thumbs"; if (!is_dir($tdir)) mkdir($tdir, 0775, true); $tpfad = "{$tdir}/{$sim}__{$file}.jpg"; if (!is_file($tpfad) || filemtime($tpfad) < filemtime($pfad)) { $src = @imagecreatefromstring((string)@file_get_contents($pfad)); if ($src) { // iPhone/Kamera-Fotos: EXIF-Orientierung berücksichtigen if (in_array($ext, ['jpg', 'jpeg'], true) && function_exists('exif_read_data')) { $exif = @exif_read_data($pfad); $o = (int)($exif['Orientation'] ?? 1); if ($o === 3) $src = imagerotate($src, 180, 0); elseif ($o === 6) $src = imagerotate($src, -90, 0); elseif ($o === 8) $src = imagerotate($src, 90, 0); } $w = imagesx($src); $h = imagesy($src); $f = min(1, 340 / max($w, $h)); $tw = max(1, (int)round($w * $f)); $th = max(1, (int)round($h * $f)); $dst = imagecreatetruecolor($tw, $th); imagecopyresampled($dst, $src, 0, 0, 0, 0, $tw, $th, $w, $h); imagejpeg($dst, $tpfad, 82); imagedestroy($src); imagedestroy($dst); } } if (is_file($tpfad)) { $pfad = $tpfad; $mime = 'image/jpeg'; } // sonst (z. B. HEIC ohne GD-Support): Original ausliefern — Safari/iPad zeigt es nativ } header('Content-Type: ' . $mime); header('Content-Length: ' . (string)filesize($pfad)); header('Cache-Control: private, max-age=86400'); readfile($pfad); exit; } /* ---------- 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 pendingNotes(string $base, string $sim, string $slug): array { $dir = "{$base}/{$sim}"; if (!is_dir($dir)) return []; $out = []; foreach (glob($dir . '/' . $slug . '__*.note.txt') as $f) { $t = trim((string)file_get_contents($f)); if ($t !== '') $out[] = $t; } return $out; } function safeName(string $s): string { $s = preg_replace('/[^A-Za-z0-9._-]+/', '-', $s); return trim(mb_substr($s, 0, 80), '-.') ?: 'foto'; } /* ---------- Löschen eines wartenden Uploads (PRG) ---------- */ $fehler = []; $geloescht = 0; if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['aktion'] ?? '') === 'loeschen') { $sim = preg_replace('/[^a-z0-9-]/', '', $_POST['sim'] ?? ''); $file = basename($_POST['file'] ?? ''); $pfad = "{$UPLOAD_BASE}/{$sim}/{$file}"; // nur echte Upload-Dateien (Muster __…), keine Notizen, kein Pfad-Ausbruch if ($sim !== '' && $file !== '' && strpos($file, '..') === false && substr($file, -9) !== '.note.txt' && is_file($pfad)) { @unlink($pfad); // zugehörigen Thumbnail-Cache mitlöschen @unlink("{$UPLOAD_BASE}/.thumbs/{$sim}__{$file}.jpg"); $geloescht = 1; } else { $fehler[] = 'Foto zum Löschen nicht gefunden.'; } if ($geloescht) { $anker = $sim . '-' . preg_replace('/__.*/', '', $file); header("Location: ?del=1&sim=" . urlencode($sim) . "#{$anker}"); exit; } } /* ---------- Upload verarbeiten (PRG-Muster) ---------- */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['aktion'] ?? '') !== 'loeschen') { $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'] ?? '')) : ''; $del = isset($_GET['del']); /* Ü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.
🗑️ Foto gelöscht.
⚠️
Kapitel: $k): ?> · Sonstiges
$k): ?>

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

📝

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.