From b13b524ad558c95278eef6b4b5c503cf3c6cabb5 Mon Sep 17 00:00:00 2001 From: Patrick Asmus Date: Sun, 1 Nov 2026 13:16:00 +0100 Subject: [PATCH] feat(php8): Umstieg auf Debian Trixie mit PHP 8.4 (WIP) PHP 7.4 ist seit 2022 EOL und bekommt keine Fixes mehr (u. a. PHP2Shell, CVE-2026-109618/109619). Ich stelle das Image deshalb auf debian:trixie-slim mit PHP 8.4.27 um. - Php8Compat.php: ergaenzt __construct() fuer PHP-4-Konstruktoren und stellt String-Offsets $str{0} auf $str[0] um (AdminServ, RemoteCP, XAseco) - RunTrackmaniaServer.sh: Patcher bei jedem Start auf die Volumes anwenden, mysqli_report(MYSQLI_REPORT_OFF) fuer den mysql_*-Shim von XAseco - Bullseye-Archive-Repositories entfernt --- Dockerfile | 12 +- assets/bin/Php8Compat.php | 197 ++++++++++++++++++++++++++++++ assets/bin/RunTrackmaniaServer.sh | 17 +++ 3 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 assets/bin/Php8Compat.php diff --git a/Dockerfile b/Dockerfile index 9e010c6..96e8733 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,11 @@ -FROM debian:bullseye-slim +# Debian 13 Trixie liefert PHP 8.4 mit regulaeren Security-Updates +FROM debian:trixie-slim RUN mkdir /opt/tmserver WORKDIR /opt/tmserver # Alle benoetigten Pakete in einem Layer installieren und Cache aufraeumen -# Debian Bullseye Archive-Repositories konfigurieren (Bullseye ist archiviert) -RUN echo "deb http://archive.debian.org/debian bullseye main" > /etc/apt/sources.list \ - && echo "deb http://archive.debian.org/debian-security bullseye-security main" >> /etc/apt/sources.list \ - && echo "Acquire::Check-Valid-Until false;" > /etc/apt/apt.conf.d/99no-check-valid-until - RUN apt-get update && apt-get install -y --no-install-recommends \ unzip \ apache2 \ @@ -53,6 +49,10 @@ COPY assets/bin/RunTrackmaniaServer.sh /opt/tmserver/ RUN sed -i 's/\r$//' /opt/tmserver/RunTrackmaniaServer.sh \ && chmod +x /opt/tmserver/RunTrackmaniaServer.sh +# PHP-8-Kompatibilitaet: ergaenzt __construct() fuer PHP-4-Konstruktoren +# (wird bei jedem Start auf die Volumes angewendet) +COPY assets/bin/Php8Compat.php /opt/tmserver/ + COPY assets/bin/XAsecoHealthcheck.sh /opt/tmserver/ RUN sed -i 's/\r$//' /opt/tmserver/XAsecoHealthcheck.sh \ && chmod +x /opt/tmserver/XAsecoHealthcheck.sh diff --git a/assets/bin/Php8Compat.php b/assets/bin/Php8Compat.php new file mode 100644 index 0000000..d44ee6f --- /dev/null +++ b/assets/bin/Php8Compat.php @@ -0,0 +1,197 @@ + [ ...] + * + * PHP 8 behandelt Methoden mit dem Namen der Klasse (PHP-4-Konstruktoren) + * nicht mehr als Konstruktor. Die alten Tools nutzen das an vielen Stellen, + * Objekte blieben dadurch uninitialisiert. + * + * Fuer jede Klasse mit PHP-4-Konstruktor und ohne __construct() wird direkt + * nach der oeffnenden Klammer ein __construct() eingefuegt, der die alte + * Methode aufruft. Die alte Methode bleibt erhalten, damit Aufrufe wie + * parent::IXR_Client(...) weiter funktionieren. + * + * Ausserdem werden String-Offsets mit geschweiften Klammern ($str{0}) auf + * eckige Klammern umgestellt (unter PHP 8 ein Parse-Error). + * + * Idempotent: Klassen mit vorhandenem __construct() werden uebersprungen. + * Dateien werden in-place geschrieben (Besitzer und Rechte bleiben erhalten). + */ + +if ($argc < 2) { + fwrite(STDERR, "Aufruf: php {$argv[0]} [...]\n"); + exit(1); +} + +$totalFiles = 0; +$totalClasses = 0; +$totalCurly = 0; + +for ($i = 1; $i < $argc; $i++) { + $dir = $argv[$i]; + if (!is_dir($dir)) continue; + $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)); + foreach ($it as $file) { + if (strtolower($file->getExtension()) !== 'php') continue; + $path = $file->getPathname(); + $src = file_get_contents($path); + if ($src === false) continue; + list($new, $count) = patchConstructors($src); + list($new, $curly) = patchCurlyOffsets($new); + if ($new !== $src) { + file_put_contents($path, $new); + $totalFiles++; + $totalClasses += $count; + $totalCurly += $curly; + echo " $path: $count Konstruktor(en), $curly Offset(s)\n"; + } + } +} + +echo " PHP-8-Kompatibilitaet: $totalClasses Konstruktor(en), $totalCurly Offset(s) in $totalFiles Datei(en) angepasst.\n"; + +/** + * Ersetzt String-Offsets mit geschweiften Klammern ($str{0}) durch eckige + * Klammern ($str[0]); unter PHP 8 ist die alte Syntax ein Parse-Error. + * Erfasst wird nur "{" direkt nach einer Variablen oder "]" (ohne + * Leerzeichen), dort hat "{" keine andere Bedeutung. + * + * Liefert [neuer Quelltext, Anzahl ersetzter Offsets]. + */ +function patchCurlyOffsets($src) +{ + if (strpos($src, '{') === false) return array($src, 0); + $tokens = @token_get_all($src); + $n = count($tokens); + $count = 0; + for ($i = 1; $i < $n; $i++) { + if ($tokens[$i] !== '{') continue; + $prev = $tokens[$i - 1]; + if (!($prev === ']' || (is_array($prev) && $prev[0] === T_VARIABLE))) continue; + // Passende schliessende Klammer suchen + $depth = 0; + for ($j = $i; $j < $n; $j++) { + $t = $tokens[$j]; + if ($t === '{' || (is_array($t) && in_array($t[0], array(T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES), true))) $depth++; + elseif ($t === '}') { $depth--; if ($depth === 0) break; } + } + if ($j >= $n) continue; + $tokens[$i] = '['; + $tokens[$j] = ']'; + $count++; + } + if ($count === 0) return array($src, 0); + $out = ''; + foreach ($tokens as $t) $out .= is_array($t) ? $t[1] : $t; + return array($out, $count); +} + +/** + * Liefert [neuer Quelltext, Anzahl eingefuegter Konstruktoren]. + */ +function patchConstructors($src) +{ + if (stripos($src, 'class') === false) return array($src, 0); + $eol = (strpos($src, "\r\n") !== false) ? "\r\n" : "\n"; + $tokens = @token_get_all($src); + $n = count($tokens); + $inserts = array(); // Byte-Offset => einzufuegender Text + + // Byte-Offsets aller Tokens berechnen + $offsets = array(); + $pos = 0; + for ($i = 0; $i < $n; $i++) { + $offsets[$i] = $pos; + $pos += strlen(is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]); + } + + for ($i = 0; $i < $n; $i++) { + if (!is_array($tokens[$i]) || $tokens[$i][0] !== T_CLASS) continue; + + // "Foo::class" und anonyme Klassen ("new class") ignorieren + $prev = prevSignificant($tokens, $i); + if ($prev !== null && is_array($tokens[$prev]) && in_array($tokens[$prev][0], array(T_DOUBLE_COLON, T_NEW), true)) continue; + + $nameIdx = nextSignificant($tokens, $i); + if ($nameIdx === null || !is_array($tokens[$nameIdx]) || $tokens[$nameIdx][0] !== T_STRING) continue; + $className = $tokens[$nameIdx][1]; + + // Oeffnende Klammer des Klassenkoerpers suchen + $open = $nameIdx; + while ($open < $n && $tokens[$open] !== '{') $open++; + if ($open >= $n) continue; + + // Methoden auf Klassenebene einsammeln + $depth = 0; + $hasConstruct = false; + $oldCtor = null; + for ($j = $open; $j < $n; $j++) { + $t = $tokens[$j]; + if ($t === '{' || (is_array($t) && in_array($t[0], array(T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES), true))) { + $depth++; + } elseif ($t === '}') { + $depth--; + if ($depth === 0) break; + } elseif ($depth === 1 && is_array($t) && $t[0] === T_FUNCTION) { + $k = nextSignificant($tokens, $j); + if ($k !== null && $tokens[$k] === '&') $k = nextSignificant($tokens, $k); + if ($k === null || !is_array($tokens[$k])) continue; + $method = $tokens[$k][1]; + if (strcasecmp($method, '__construct') === 0) $hasConstruct = true; + if (strcasecmp($method, $className) === 0) $oldCtor = $k; + } + } + if ($hasConstruct || $oldCtor === null) continue; + + // Parameterliste der alten Methode uebernehmen + $p = $oldCtor; + while ($p < $n && $tokens[$p] !== '(') $p++; + $params = ''; + $vars = array(); + $byRef = false; + $pd = 0; + for ($q = $p; $q < $n; $q++) { + $t = $tokens[$q]; + if ($t === '(') { $pd++; if ($pd === 1) continue; } + if ($t === ')') { $pd--; if ($pd === 0) break; } + $txt = is_array($t) ? $t[1] : $t; + $params .= $txt; + if ($pd === 1 && is_array($t) && $t[0] === T_VARIABLE) $vars[] = $t[1]; + if ($pd === 1 && $t === '&') $byRef = true; + } + + // Ohne Referenzparameter func_get_args() nutzen (gleiche Semantik bei + // optionalen Parametern), sonst die Variablen direkt durchreichen. + $call = $byRef ? implode(', ', $vars) : '...func_get_args()'; + $shim = "{$eol}\t// tmserver-docker: Php8Compat{$eol}" + . "\tfunction __construct(" . trim($params) . ") { \$this->{$tokens[$oldCtor][1]}($call); }"; + $inserts[$offsets[$open] + 1] = $shim; + } + + if (empty($inserts)) return array($src, 0); + krsort($inserts); + foreach ($inserts as $off => $txt) { + $src = substr($src, 0, $off) . $txt . substr($src, $off); + } + return array($src, count($inserts)); +} + +function nextSignificant($tokens, $i) +{ + for ($j = $i + 1, $n = count($tokens); $j < $n; $j++) { + if (is_array($tokens[$j]) && in_array($tokens[$j][0], array(T_WHITESPACE, T_COMMENT, T_DOC_COMMENT), true)) continue; + return $j; + } + return null; +} + +function prevSignificant($tokens, $i) +{ + for ($j = $i - 1; $j >= 0; $j--) { + if (is_array($tokens[$j]) && in_array($tokens[$j][0], array(T_WHITESPACE, T_COMMENT, T_DOC_COMMENT), true)) continue; + return $j; + } + return null; +} diff --git a/assets/bin/RunTrackmaniaServer.sh b/assets/bin/RunTrackmaniaServer.sh index 99f7293..919ce40 100644 --- a/assets/bin/RunTrackmaniaServer.sh +++ b/assets/bin/RunTrackmaniaServer.sh @@ -409,6 +409,23 @@ if [ "$XASECO_ENABLED" = "true" ]; then fi fi +# ============================================================ +# PHP-8-Kompatibilitaet (bei jedem Start, auch fuer bestehende Volumes) +# ============================================================ +# AdminServ, RemoteCP und XAseco stammen aus PHP-5-Zeiten. Php8Compat.php +# ergaenzt __construct() fuer PHP-4-Konstruktoren und stellt String-Offsets +# mit geschweiften Klammern ($str{0}) auf eckige Klammern um. Idempotent. +echo "==> Pruefe PHP-8-Kompatibilitaet..." +php /opt/tmserver/Php8Compat.php "$ADMINSERV_DIR" /opt/tmserver/xaseco | tail -n 1 + +# XAseco: mysql_*-Shim (modernizer.inc.php) erwartet false bei SQL-Fehlern. +# Seit PHP 8.1 wirft mysqli standardmaessig Exceptions -> abschalten. +XASECO_MODERNIZER="/opt/tmserver/xaseco/includes/modernizer.inc.php" +if [ -f "$XASECO_MODERNIZER" ] && ! grep -q 'mysqli_report' "$XASECO_MODERNIZER"; then + sed -i '0,/^