274 lines
9.9 KiB
PHP
274 lines
9.9 KiB
PHP
|
|
<?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);
|