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') && !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.

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 herunter, prüft die Prüfsumme und entpackt sie hierher:

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.