Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
This commit is contained in:
273
bin/filesgallery-update.php
Normal file
273
bin/filesgallery-update.php
Normal file
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Files Gallery (Dateiablage /dateien) aktualisieren — App-Datei und self-hosted Assets.
|
||||
*
|
||||
* Warum ein Skript: die App lädt ihre Assets sonst vom jsDelivr-CDN (index.php:965), was
|
||||
* gegen Regel 1 verstößt. Self-Hosting ist offiziell vorgesehen, muss aber bei JEDEM
|
||||
* Versionswechsel wiederholt werden, weil die Asset-Pfade die Version enthalten
|
||||
* (files.photo.gallery@<version>/...). 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);
|
||||
Reference in New Issue
Block a user