/...). Dazu kommt der TSV-PATCH in index.php, den ein * Update sonst wegwirft. Beides von Hand ist genau der Schritt, der einmal vergessen wird. * * NUR CLI/Build — nie im Request-Pfad. Die einzige ausgehende Verbindung ist jsDelivr, * und die passiert hier, nicht im Browser des Besuchers. * * Aufruf: * php bin/filesgallery-update.php --check nur prüfen (Patch da? Assets zur Version da?) * php bin/filesgallery-update.php --assets Assets zur vendorten Version neu holen * php bin/filesgallery-update.php neue index.php holen, patchen, Assets holen * * Nach einem echten Versionssprung: Login, Upload und Vorschau einmal durchklicken und * die alten Asset-Verzeichnisse löschen (das Skript sagt, welche das sind). */ if (PHP_SAPI !== 'cli') { exit(1); } require dirname(__DIR__) . '/app/bootstrap.php'; const FG_APP = PUBLIC_PATH . '/dateien/index.php'; const FG_ASSETS_DIR = PUBLIC_PATH . '/assets/filesgallery'; const FG_APP_URL = 'https://cdn.jsdelivr.net/npm/files.photo.gallery/index.php'; const FG_LOG = STORAGE_PATH . '/logs/filesgallery.log'; // Der Patch, ohne den die App bei abweichendem Arbeitsverzeichnis ohne Login startet. const FG_PATCH_NEEDLE = "public static \$localconfigpath = '_filesconfig.php';"; const FG_PATCH_MARKER = "public static \$localconfigpath = __DIR__ . '/_filesconfig.php';"; const FG_PATCH_REPLACEMENT = <<<'PHP' // TSV-PATCH (einzige Änderung an dieser Datei): absolut statt relativ. // Der Original-Pfad wird gegen das aktuelle Arbeitsverzeichnis aufgelöst. Weicht das ab // (z. B. unter `php -S` mit Router-Script), findet die App _filesconfig.php nicht und // startet mit ihrer Default-Config — also OHNE Login und mit falschen Pfaden. // Nebeneffekt: der Selbst-Ausschluss in Path::is_exclude() vergleicht diesen Wert mit // einem Basename und greift dadurch nicht mehr. Belanglos, weil unser `root` // (storage/dateien/uploads) außerhalb dieses Verzeichnisses liegt. // Nach jedem Update neu setzen: `php bin/filesgallery-update.php` erledigt und prüft das. public static $localconfigpath = __DIR__ . '/_filesconfig.php'; PHP; $mode = $argv[1] ?? ''; if (!in_array($mode, ['', '--check', '--assets'], true)) { fwrite(STDERR, "Unbekannte Option: {$mode}\nAufruf: php bin/filesgallery-update.php [--check|--assets]\n"); exit(1); } $errors = 0; function fg_log(string $line): void { @file_put_contents(FG_LOG, date('c') . ' ' . $line . "\n", FILE_APPEND); } function fg_fail(string $message): void { global $errors; $errors++; fwrite(STDERR, "✗ {$message}\n"); fg_log('FEHLER ' . $message); } /** Datei von jsDelivr holen; gibt den Rohinhalt zurück oder null. */ function fg_fetch(string $url): ?string { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 120, // die Assets sind ein paar MB CURLOPT_USERAGENT => 'tsv08kulmbach-filesgallery-update/1.0', ]); $body = curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $error = curl_error($ch); if (!is_string($body) || $body === '' || $status !== 200) { fg_fail("Download fehlgeschlagen ({$status}" . ($error !== '' ? ", {$error}" : '') . "): {$url}"); return null; } return $body; } /** Version aus dem Quelltext der App lesen. */ function fg_version(string $source): ?string { return preg_match('/public static \$version = \'([0-9][^\']*)\'/', $source, $m) === 1 ? $m[1] : null; } /** * Den TSV-PATCH anwenden. Idempotent: ist er schon drin, bleibt die Datei unverändert. * Fehlt beides (Patch und Original-Zeile), bricht das Skript ab — dann hat die App ihre * Struktur geändert und der Patch muss von Hand nachgezogen werden. */ function fg_patch(string $source): ?string { if (str_contains($source, FG_PATCH_MARKER)) { return $source; } if (!str_contains($source, FG_PATCH_NEEDLE)) { fg_fail('Patch-Stelle nicht gefunden ($localconfigpath) — index.php von Hand prüfen, NICHT ungepatcht ausliefern.'); return null; } // Die Original-Zeile trägt einen Kommentar hinter dem Semikolon; den behalten wir. return (string) preg_replace( '/^\h*' . preg_quote(FG_PATCH_NEEDLE, '/') . '/m', FG_PATCH_REPLACEMENT, $source, 1 ); } /** assets.zip zur Version holen und nach public/assets/filesgallery/ entpacken. */ function fg_assets(string $version): void { $url = "https://cdn.jsdelivr.net/npm/files.photo.gallery@{$version}/_files/assets/assets.zip"; $zipData = fg_fetch($url); if ($zipData === null) { return; } $tmp = tempnam(sys_get_temp_dir(), 'fg-assets-'); if ($tmp === false || file_put_contents($tmp, $zipData) === false) { fg_fail('Konnte assets.zip nicht zwischenspeichern.'); return; } $zip = new ZipArchive(); if ($zip->open($tmp) !== true) { fg_fail('assets.zip ist kein lesbares ZIP-Archiv.'); @unlink($tmp); return; } if (!is_dir(FG_ASSETS_DIR) && !mkdir(FG_ASSETS_DIR, 0775, true) && !is_dir(FG_ASSETS_DIR)) { fg_fail('Kann ' . FG_ASSETS_DIR . ' nicht anlegen.'); $zip->close(); @unlink($tmp); return; } $written = 0; for ($i = 0; $i < $zip->numFiles; $i++) { $name = (string) $zip->getNameIndex($i); // Manche Archive tragen den _files/assets-Pfad noch mit sich; wir wollen den Inhalt flach. $rel = (string) preg_replace('#^(\./)?(_files/)?assets/#', '', $name); $rel = ltrim(str_replace('\\', '/', $rel), '/'); // Zip-Slip: alles mit Parent-Traversal oder absolutem Pfad wird verworfen. if ($rel === '' || str_contains($rel, '../')) { continue; } $target = FG_ASSETS_DIR . '/' . $rel; if (str_ends_with($name, '/')) { if (!is_dir($target)) { @mkdir($target, 0775, true); } continue; } $dir = dirname($target); if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) { fg_fail("Kann Verzeichnis nicht anlegen: {$dir}"); continue; } $stream = $zip->getStream($name); if ($stream === false) { fg_fail("Konnte Eintrag nicht lesen: {$name}"); continue; } if (@file_put_contents($target, $stream) === false) { fg_fail("Konnte Datei nicht schreiben: {$target}"); } else { $written++; } fclose($stream); } $zip->close(); @unlink($tmp); echo "✓ Assets entpackt ({$written} Dateien) → " . str_replace(ROOT_PATH . '/', '', FG_ASSETS_DIR) . "\n"; fg_log("Assets {$version} entpackt ({$written} Dateien)"); } /** Prüfen, ob Patch und Assets zur vendorten Version passen. */ function fg_check(string $version, string $source): void { if (str_contains($source, FG_PATCH_MARKER)) { echo "✓ TSV-PATCH in public/dateien/index.php vorhanden\n"; } else { fg_fail('TSV-PATCH fehlt in public/dateien/index.php — die App würde ohne Login starten.'); } $css = FG_ASSETS_DIR . "/files.photo.gallery@{$version}/css/files.css"; if (is_file($css)) { echo "✓ Assets für Version {$version} vorhanden\n"; } else { fg_fail("Assets für Version {$version} fehlen (erwartet: " . str_replace(ROOT_PATH . '/', '', $css) . ") — php bin/filesgallery-update.php --assets"); } // Verirrte Assets anderer Versionen melden, damit sie nicht mitgeschleppt werden. foreach (glob(FG_ASSETS_DIR . '/files.photo.gallery@*') ?: [] as $dir) { if (basename($dir) !== "files.photo.gallery@{$version}") { echo "· Alte Assets, können weg: " . str_replace(ROOT_PATH . '/', '', $dir) . "\n"; } } } // --- Ablauf --------------------------------------------------------------------------- if (!is_file(FG_APP)) { fwrite(STDERR, '✗ ' . FG_APP . " fehlt.\n"); exit(1); } $source = (string) file_get_contents(FG_APP); if ($mode === '' ) { // Vollupdate: neue App-Datei holen, patchen, dann Assets zur neuen Version. $fresh = fg_fetch(FG_APP_URL); if ($fresh !== null) { $newVersion = fg_version($fresh); if ($newVersion === null) { fg_fail('Version im Download nicht gefunden — Datei nicht übernommen.'); } else { $patched = fg_patch($fresh); if ($patched !== null) { $old = fg_version($source) ?? '?'; if ($patched === $source) { echo "· Version {$newVersion} ist schon aktuell und gepatcht\n"; } elseif (file_put_contents(FG_APP, $patched) === false) { fg_fail('Konnte ' . FG_APP . ' nicht schreiben.'); } else { echo "✓ index.php aktualisiert: {$old} → {$newVersion} (gepatcht)\n"; fg_log("index.php {$old} → {$newVersion}"); $source = $patched; } } } } } $version = fg_version($source); if ($version === null) { fwrite(STDERR, "✗ Version in public/dateien/index.php nicht lesbar.\n"); exit(1); } if ($mode !== '--check') { fg_assets($version); } fg_check($version, $source); if ($errors > 0) { fwrite(STDERR, "\n{$errors} Problem(e) — nicht ausliefern, bis sie behoben sind.\n"); exit(1); } echo "\nFiles Gallery {$version}: alles an seinem Platz.\n"; exit(0);