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
This commit is contained in:
Patrick Asmus committed 2026-11-01 13:16:00 +01:00
1 parent 5ca2f1587f
commit b13b524ad5
3 files changed
+220 -6

No files matched your search

+6 -6
View File
@@ -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
+197
View File
@@ -0,0 +1,197 @@
<?php
/**
* Php8Compat.php - PHP-8-Kompatibilitaet fuer AdminServ, RemoteCP und XAseco
*
* Aufruf: php Php8Compat.php <verzeichnis> [<verzeichnis> ...]
*
* 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]} <verzeichnis> [...]\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;
}
+17
View File
@@ -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,/^<?php/s//<?php\nmysqli_report(MYSQLI_REPORT_OFF); \/\/ tmserver-docker: PHP 8.1+/' "$XASECO_MODERNIZER"
echo " modernizer.inc.php: mysqli_report(MYSQLI_REPORT_OFF) ergaenzt."
fi
# ============================================================
# XAseco: TeamSpeak3-Plugin Gateway aktualisieren (fuer bestehende Volumes)
# ============================================================