Installer: sechs Bildschirme, und eine Datei davor
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s

/install fuehrt von einer leeren Installation zu einer Website:
Systempruefung, Datenbank (SQLite oder MySQL, Verbindung sofort geprueft),
Migration im Prozess ohne Konsole, Website mit Startseite, Bereichen, Menue
sowie Impressum und Datenschutzgeruest, erstes Konto, Abschlussliste.

Jeder Schritt schreibt sein Ergebnis sofort, damit ein Neuladen nichts
verliert. Gesperrt wird ueber den Zustand der Datenbank - gibt es ein
Konto, antwortet /install mit 404. Eine Datei koennte jemand loeschen.

Solange nichts installiert ist, fuehrt jede Adresse zum Installer statt zu
einem 500 aus einer fehlenden Tabelle. Und weil ohne APP_SECRET nicht
einmal die Seite bootet, die das erklaeren wuerde, erzeugt der erste Start
das Geheimnis selbst.

scripts/install.php ist die einzelne Datei davor: Adresse und SHA-256
stehen darin (vom Buildskript eingetragen), ohne passende Pruefsumme wird
nicht entpackt, jeder Archiveintrag wird gegen das Zielverzeichnis
geprueft, und danach loescht sie sich selbst.

Geprueft: 379 Tests unter PHP 8.2 und 8.4, darunter der ganze Ablauf durch
die HTTP-Schicht, neun ZIP-Slip-Varianten, axe ohne Verstoss auf allen
Installationsbildschirmen, und das entpackte Releasearchiv startet in den
Installer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-12 23:11:50 +02:00
1 parent c3bff7d936
commit e34b4dca67
25 files changed
+1937 -7

No files matched your search

+25 -2
View File
@@ -15,7 +15,10 @@
* would mean an archive with PHPUnit in it.
*/
$root = dirname(__DIR__);
$options = getopt('', ['out::', 'keep-dev']);
$options = getopt('', ['out::', 'keep-dev', 'url::']);
// Wohin das Archiv beim Veröffentlichen gelegt wird. Steht fest in install.php, deshalb muss
// sie hier bekannt sein - eine Adresse, die erst im Browser abgefragt wird, wäre eine Lücke.
$releaseBase = rtrim($options['url'] ?? 'https://git.kant-it.de/kantarci/KantSites/releases/download', '/');
$outDirectory = $root.'/'.trim($options['out'] ?? 'var/releases', '/');
$version = trim((string) file_get_contents($root.'/VERSION'));
if (1 !== preg_match('/^\d+\.\d+\.\d+$/D', $version)) {
@@ -87,6 +90,14 @@ foreach ($include as $entry) {
$files += $copy($root.'/'.$entry, $staging.'/'.$entry);
}
}
// Die .env des Entwicklungsrechners gehört nicht in ein Release; gebraucht wird trotzdem eine,
// denn ohne sie bootet die Anwendung nicht einmal so weit, dass der Installer erscheint. Also
// wird die Vorlage zur .env des Releases - mit Produktionswerten und ohne Geheimnis. Das
// Geheimnis erzeugt der erste Aufruf selbst, die Zugangsdaten schreibt der Installer nach
// .env.local.
copy($root.'/.env.example', $staging.'/.env');
++$files;
echo "Quelldateien: {$files}\n";
// The dependencies, without the development ones - that is the whole point of building here.
@@ -137,4 +148,16 @@ file_put_contents($archive.'.sha256', $hash.' '.basename($archive)."\n");
printf("\nArchiv: %s\n", $archive);
printf("Dateien: %d, Größe: %.1f MB\n", $entries, filesize($archive) / 1048576);
printf("SHA-256: %s\n", $hash);
echo "\nDie Prüfsumme gehört in install.php, damit ein manipuliertes Archiv nicht entpackt wird.\n";
// Und der einzelne Dateidownload: dieselbe install.php, nur mit eingetragener Adresse und
// Prüfsumme. Ohne beides verweigert sie die Arbeit, deshalb entsteht sie erst hier.
$installer = strtr((string) file_get_contents($root.'/scripts/install.php'), [
"const KANT_ARCHIVE_URL = '';" => sprintf("const KANT_ARCHIVE_URL = '%s';", $releaseBase.'/v'.$version.'/'.basename($archive)),
"const KANT_ARCHIVE_SHA256 = '';" => sprintf("const KANT_ARCHIVE_SHA256 = '%s';", $hash),
"const KANT_VERSION = '';" => sprintf("const KANT_VERSION = '%s';", $version),
]);
file_put_contents($outDirectory.'/install.php', $installer);
printf("Installer: %s (lädt %s)\n", $outDirectory.'/install.php', $releaseBase.'/v'.$version.'/'.basename($archive));
echo "\nBeides gehört auf die Releaseseite: das Archiv unter genau dieser Adresse, und install.php zum Herunterladen.\n";
echo "Die Prüfsumme steckt jetzt in install.php, damit ein manipuliertes Archiv nicht entpackt wird.\n";