1 && str_starts_with($parts[0], 'kantsites-')) {
array_shift($parts);
}
foreach ($parts as $part) {
if ('' === $part || '.' === $part || '..' === $part || str_contains($part, "\0") || str_contains($part, ':')) {
return null;
}
}
if ([] === $parts) {
return null;
}
$base = rtrim(str_replace('\\', '/', $target), '/');
$path = $base.'/'.implode('/', $parts);
// Doppelt geprüft: der zusammengesetzte Pfad muss unterhalb des Ziels beginnen.
return str_starts_with($path, $base.'/') ? $path : null;
}
/** Lädt eine Adresse über HTTPS mit Zertifikatsprüfung in eine Datei. Gibt eine Meldung oder null. */
function kant_download(string $url, string $file): ?string
{
if (!str_starts_with($url, 'https://')) {
return 'Die Adresse des Archivs ist keine HTTPS-Adresse.';
}
$handle = fopen($file, 'wb');
if (false === $handle) {
return 'Die heruntergeladene Datei ließ sich nicht anlegen: '.$file;
}
try {
if (function_exists('curl_init')) {
$curl = curl_init($url);
curl_setopt_array($curl, [
CURLOPT_FILE => $handle,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CONNECTTIMEOUT => 20,
CURLOPT_TIMEOUT => 900,
CURLOPT_USERAGENT => 'KantSites-Installer',
]);
$ok = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$error = curl_error($curl);
curl_close($curl);
if (!$ok) {
return 'Der Download ist gescheitert: '.$error;
}
if (200 !== $status) {
return 'Der Server hat mit Status '.$status.' geantwortet statt mit 200.';
}
return null;
}
if (!filter_var(ini_get('allow_url_fopen'), FILTER_VALIDATE_BOOL)) {
return 'Weder curl noch allow_url_fopen ist verfügbar. Ohne eines von beidem kann diese Datei nichts herunterladen; dann bleibt der Weg über FTP mit dem entpackten Archiv.';
}
$context = stream_context_create(['http' => ['timeout' => 900, 'follow_location' => 1, 'max_redirects' => 3], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true]]);
$source = @fopen($url, 'rb', false, $context);
if (false === $source) {
return 'Das Archiv ließ sich nicht öffnen. Prüfe, ob dieser Server nach außen verbinden darf.';
}
stream_copy_to_stream($source, $handle);
fclose($source);
return null;
} finally {
if (is_resource($handle)) {
fclose($handle);
}
}
}
/** Entpackt das Archiv; gibt die Anzahl der Dateien zurück oder wirft mit einer lesbaren Meldung. */
function kant_unpack(string $archive, string $target): int
{
if (!class_exists('ZipArchive')) {
throw new RuntimeException('Die PHP-Erweiterung zip fehlt. Ohne sie lässt sich das Archiv nicht entpacken.');
}
$zip = new ZipArchive();
if (true !== $zip->open($archive)) {
throw new RuntimeException('Das Archiv ließ sich nicht öffnen. Der Download ist wahrscheinlich unvollständig.');
}
$written = 0;
try {
for ($index = 0; $index < $zip->numFiles; ++$index) {
$name = (string) $zip->getNameIndex($index);
$path = kant_target_path($name, $target);
if (null === $path) {
if (str_ends_with($name, '/')) {
continue;
}
throw new RuntimeException('Das Archiv enthält einen Eintrag, der außerhalb des Zielverzeichnisses landen würde: '.$name);
}
if (!is_dir(dirname($path)) && !mkdir(dirname($path), 0o775, true) && !is_dir(dirname($path))) {
throw new RuntimeException('Verzeichnis ließ sich nicht anlegen: '.dirname($path));
}
$stream = $zip->getStream($name);
if (false === $stream) {
throw new RuntimeException('Eintrag ließ sich nicht lesen: '.$name);
}
$out = fopen($path, 'wb');
if (false === $out) {
fclose($stream);
throw new RuntimeException('Datei ließ sich nicht schreiben: '.$path);
}
stream_copy_to_stream($stream, $out);
fclose($stream);
fclose($out);
++$written;
}
} finally {
$zip->close();
}
return $written;
}
/** Die Vorprüfung: alles, was fehlschlagen kann, bevor irgendetwas geschrieben wird. */
function kant_checks(string $target): array
{
$checks = [];
$add = static function (string $label, bool $ok, string $hint) use (&$checks): void {
$checks[] = ['label' => $label, 'ok' => $ok, 'hint' => $hint];
};
$add('PHP-Fassung', PHP_VERSION_ID >= 80200, 'Gefunden: '.PHP_VERSION.'. Gebraucht wird mindestens 8.2 – beim Hoster umstellen.');
$add('Schreibrecht im Zielverzeichnis', is_writable($target), $target.' ist nicht beschreibbar. Im FTP-Programm die Rechte auf 0775 setzen.');
$add('ZIP-Erweiterung', class_exists('ZipArchive'), 'Ohne die Erweiterung zip lässt sich das Archiv nicht entpacken.');
$add('Verbindung nach außen', function_exists('curl_init') || filter_var(ini_get('allow_url_fopen'), FILTER_VALIDATE_BOOL), 'Weder curl noch allow_url_fopen ist verfügbar; dann bleibt nur der Weg über FTP.');
$add('Adresse und Prüfsumme eingetragen', '' !== KANT_ARCHIVE_URL && 64 === strlen(KANT_ARCHIVE_SHA256), 'Diese Datei ist unfertig: sie stammt nicht aus einem Release. Lade install.php von der Releaseseite herunter.');
$free = @disk_free_space($target);
$add('Freier Platz', false === $free || $free > 80 * 1024 * 1024, 'Es sind weniger als 80 MB frei. Das Archiv braucht Platz zum Herunterladen und zum Entpacken.');
$add('Verzeichnis ist leer genug', !is_file($target.'/public/index.php') && !is_file($target.'/.env.local'), 'Hier liegt schon eine Installation. Diese Datei überschreibt nichts – bei einem Update ist der Weg das Releasearchiv von Hand oder der Updater in der Verwaltung.');
return $checks;
}
/** Der Ablauf, wenn der Knopf gedrückt wurde. Gibt [Erfolg, Meldung] zurück. */
function kant_run(string $target): array
{
$archive = $target.'/kantsites-download-'.bin2hex(random_bytes(4)).'.zip';
try {
$error = kant_download(KANT_ARCHIVE_URL, $archive);
if (null !== $error) {
return [false, $error];
}
$hash = hash_file('sha256', $archive);
if (!hash_equals(KANT_ARCHIVE_SHA256, (string) $hash)) {
// Nicht entpacken, nicht behalten: eine Datei, die nicht das ist, was sie sein soll,
// hat auf dem Server nichts verloren.
return [false, 'Die Prüfsumme des heruntergeladenen Archivs stimmt nicht. Erwartet: '.KANT_ARCHIVE_SHA256.', erhalten: '.$hash.'. Es wurde nichts entpackt.'];
}
$files = kant_unpack($archive, $target);
return [true, 'Entpackt: '.$files.' Dateien.'];
} catch (Throwable $e) {
return [false, $e->getMessage()];
} finally {
@unlink($archive);
}
}
// Im Test wird diese Datei eingebunden, um die Pfadprüfung und das Entpacken zu prüfen, ohne dass
// etwas heruntergeladen wird.
if ('cli' === PHP_SAPI) {
return;
}
$target = __DIR__;
$checks = kant_checks($target);
$ready = [] === array_filter($checks, static fn (array $check): bool => !$check['ok']);
$done = false;
$message = null;
if ('POST' === ($_SERVER['REQUEST_METHOD'] ?? 'GET') && $ready) {
@set_time_limit(0);
[$done, $message] = kant_run($target);
if ($done) {
// Erst jetzt: die Datei hat ihre Arbeit getan und ist ab hier nur noch ein offener Weg,
// Code auf den Server zu legen.
@unlink(__FILE__);
}
}
$e = static fn (?string $value): string => htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
header('Content-Type: text/html; charset=UTF-8');
header('X-Robots-Tag: noindex, nofollow');
?>
KantSites installieren
Die Dateien liegen.
Diese Datei hat sich selbst gelöscht. Weiter geht es mit der Einrichtung: Datenbank, Konto, Website.
= $e($message) ?>
Zur Einrichtung →
Führt der Verweis ins Leere, zeigt die Domain nicht auf public/. Das muss beim Hoster eingestellt werden – ohne das wären auch .env und var/ über das Netz erreichbar.
KantSites installieren
Diese Datei lädt Fassung = $e(KANT_VERSION ?: 'unbekannt') ?> herunter, prüft die Prüfsumme und entpackt sie hierher: = $e($target) ?>
= $e($message) ?>
-
= $check['ok'] ? '✓' : '!' ?>
= $e($check['label']) ?>= $check['ok'] ? '' : ' – '.$e($check['hint']) ?>
Das dauert je nach Anbindung eine halbe bis mehrere Minuten. Adresse und Prüfsumme stehen fest in dieser Datei; ein Archiv mit abweichender Prüfsumme wird nicht entpackt.