Show a maintenance page instead of a blank 500 and update in one click
Emergency layer logs fatal errors to var/log/notfall.log and answers 503. The system page checks for a new release on load, shows its notes, runs backup, download, switch, migration and health check in one flow with live progress, and switches back automatically when the new version fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
339a48c810
commit
a6a334fe68
15 files changed
+973
-42
No files matched your search
+183
-5
@@ -51,6 +51,21 @@ final class ArchiveUpdate
|
||||
/** Das Protokoll - dieselbe Datei, in die auch der Updater über Git schreibt. */
|
||||
private const PROTOKOLL = 'var/updates.log';
|
||||
|
||||
/**
|
||||
* Der Fortschritt für die Systemseite (ADR 113): Phase, Meldung, von welcher auf welche Fassung.
|
||||
* Die Seite fragt ihn alle zwei Sekunden ab, während eine andere Anfrage das Update ausführt -
|
||||
* das Vorbild ist reiseSoft.
|
||||
*/
|
||||
public const STATUS = 'var/update/status.json';
|
||||
|
||||
/** Die Phasen, die die Systemseite als Schritte zeigt, in ihrer Reihenfolge. */
|
||||
public const PHASEN = ['sichern', 'laden', 'umschalten', 'migrieren', 'pruefen'];
|
||||
|
||||
/** Was mergePublic() überschrieben oder neu angelegt hat - für das Zurückschalten. */
|
||||
private array $publicAlt = [];
|
||||
private array $publicNeu = [];
|
||||
private ?string $publicSicherung = null;
|
||||
|
||||
public function __construct(
|
||||
private string $root,
|
||||
private Updater $updater,
|
||||
@@ -85,6 +100,10 @@ final class ArchiveUpdate
|
||||
$version = $treffer[1];
|
||||
|
||||
return [
|
||||
// Was die Releaseseite über die Fassung sagt - die Systemseite zeigt es vor dem Knopf.
|
||||
'name' => (string) ($daten['name'] ?? ''),
|
||||
'notes' => (string) ($daten['body'] ?? ''),
|
||||
'published' => (string) ($daten['published_at'] ?? ''),
|
||||
'version' => $version,
|
||||
'url' => sprintf('%s/%s/kantsites-%s.zip', $this->releaseBase, $tag, $version),
|
||||
'checksum' => $this->checksumOf(sprintf('%s/%s/kantsites-%s.zip.sha256', $this->releaseBase, $tag, $version)),
|
||||
@@ -118,6 +137,12 @@ final class ArchiveUpdate
|
||||
$melden = function (array &$schritte, string $schritt, bool $ok, string $detail = ''): bool {
|
||||
$schritte[] = ['step' => $schritt, 'ok' => $ok, 'detail' => $detail];
|
||||
$this->protokoll(sprintf('%s %s: %s', $ok ? 'ok ' : 'FEHLER', $schritt, $detail));
|
||||
// Ein gescheiterter Schritt vor dem Tausch: die bisherige Fassung läuft unverändert.
|
||||
// Die Sperre meldet ein anderes, laufendes Update - dessen Fortschritt bleibt stehen.
|
||||
// Ist die Releaseseite nicht erreichbar, hat nichts begonnen; dann bleibt alles, wie es ist.
|
||||
if (!$ok && !in_array($schritt, ['Sperre', 'Fassung ermitteln'], true)) {
|
||||
$this->phase('fehler', $detail.' Die bisherige Fassung läuft unverändert weiter.', ['laeuft' => false]);
|
||||
}
|
||||
|
||||
return $ok;
|
||||
};
|
||||
@@ -136,6 +161,7 @@ final class ArchiveUpdate
|
||||
|
||||
return ['ok' => true, 'steps' => $schritte];
|
||||
}
|
||||
$this->phase('sichern', 'Datenbank und Medien werden gesichert …', ['laeuft' => true, 'von' => $this->updater->version(), 'auf' => $ziel, 'seit' => gmdate('c')], true);
|
||||
|
||||
$lock = $this->updater->lock();
|
||||
if (null === $lock) {
|
||||
@@ -165,6 +191,7 @@ final class ArchiveUpdate
|
||||
try {
|
||||
$sicherung = $this->backup->create('vor dem Update');
|
||||
$melden($schritte, 'Sicherung', true, sprintf('Datenbank und Medien liegen unter var/backups/%s.', (string) ($sicherung['name'] ?? 'sicherung')));
|
||||
$this->phase('laden', sprintf('Fassung %s wird geladen …', $ziel));
|
||||
} catch (\Throwable $fehler) {
|
||||
$melden($schritte, 'Sicherung', false, 'Die Sicherung ist fehlgeschlagen: '.$fehler->getMessage().' Ohne sie wird nichts ersetzt.');
|
||||
|
||||
@@ -211,6 +238,7 @@ final class ArchiveUpdate
|
||||
}
|
||||
$melden($schritte, 'Entpacken', true, 'Vollständig: VERSION und vendor/ sind vorhanden.');
|
||||
|
||||
$this->phase('umschalten', 'Wartungsseite an, die neuen Dateien werden eingesetzt …');
|
||||
$this->updater->maintenance(true);
|
||||
$melden($schritte, 'Wartungszustand', true, 'Die Website antwortet währenddessen mit 503.');
|
||||
|
||||
@@ -229,11 +257,14 @@ final class ArchiveUpdate
|
||||
++$ersetzt;
|
||||
}
|
||||
// public/ wird Datei für Datei aktualisiert: Schriften und erzeugte Bündel gehören dem
|
||||
// Betreiber und wären sonst weg.
|
||||
// Betreiber und wären sonst weg. Was dabei überschrieben wird, kommt vorher beiseite,
|
||||
// damit ein Zurückschalten auch public/ zurückbringt.
|
||||
$this->publicSicherung = $arbeit.'/alt-public';
|
||||
$this->mergePublic($quelle.'/public', $this->root.'/public');
|
||||
// Ebenso plugins/: die mitgelieferten Plugins werden aktualisiert, ein vom Betreiber
|
||||
// installiertes Plugin bleibt, weil sein Ordner im Archiv nicht vorkommt.
|
||||
$this->mergePublic($quelle.'/plugins', $this->root.'/plugins');
|
||||
@file_put_contents($arbeit.'/public.json', json_encode(['alt' => $this->publicAlt, 'neu' => $this->publicNeu], JSON_UNESCAPED_SLASHES));
|
||||
} catch (\Throwable $fehler) {
|
||||
foreach (self::ERSETZEN as $eintrag) {
|
||||
if (is_dir($alt.'/'.$eintrag) || is_file($alt.'/'.$eintrag)) {
|
||||
@@ -265,7 +296,11 @@ final class ArchiveUpdate
|
||||
'seit' => gmdate('c'),
|
||||
'alt' => basename($arbeit),
|
||||
'token' => $token,
|
||||
// Für das Zurückschalten: von welcher Fassung, und welche Sicherung dazu gehört.
|
||||
'von' => (string) ($this->status()['von'] ?? ''),
|
||||
'sicherung' => (string) ($sicherung['name'] ?? ''),
|
||||
], JSON_UNESCAPED_SLASHES));
|
||||
$this->phase('migrieren', 'Die neuen Dateien sind da. Die Datenbank wird angepasst …');
|
||||
$melden($schritte, 'Dateien sind ersetzt', true, sprintf(
|
||||
'Fassung %s liegt bereit. Jetzt fehlen noch die Migrationen - sie laufen im nächsten Schritt mit dem neuen Code.',
|
||||
$ziel,
|
||||
@@ -302,25 +337,49 @@ final class ArchiveUpdate
|
||||
*
|
||||
* @return array{ok: bool, steps: list<array{step: string, ok: bool, detail: string}>}
|
||||
*/
|
||||
public function finish(Installation $installation): array
|
||||
/**
|
||||
* @param (callable(): ?string)|null $pruefen prüft die neue Fassung nach den Migrationen und
|
||||
* gibt null zurück oder den Grund, warum sie nicht
|
||||
* läuft - dann wird zurückgeschaltet
|
||||
*/
|
||||
public function finish(Installation $installation, ?callable $pruefen = null): array
|
||||
{
|
||||
$schritte = [];
|
||||
$offen = $this->pending();
|
||||
$this->protokoll(sprintf('--- Migrationen nach dem Update, %s UTC', gmdate('Y-m-d H:i:s')));
|
||||
$this->phase('migrieren', 'Die Datenbank wird angepasst …', ['laeuft' => true]);
|
||||
try {
|
||||
$namen = $installation->migrate();
|
||||
$schritte[] = ['step' => 'Migrationen', 'ok' => true, 'detail' => [] === $namen
|
||||
? 'Es stand keine an - die Datenbank war bereits auf dem Stand der neuen Fassung.'
|
||||
: sprintf('%d ausgeführt: %s.', count($namen), implode(', ', $namen))];
|
||||
} catch (\Throwable $fehler) {
|
||||
$schritte[] = ['step' => 'Migrationen', 'ok' => false, 'detail' => $fehler->getMessage().' Die Website bleibt im Wartungszustand, bis das geklärt ist.'];
|
||||
$schritte[] = ['step' => 'Migrationen', 'ok' => false, 'detail' => $fehler->getMessage()];
|
||||
$this->protokoll('FEHLER Migrationen: '.$fehler->getMessage());
|
||||
|
||||
return ['ok' => false, 'steps' => $schritte];
|
||||
return $this->zurueckschalten($offen, 'Die Datenbank ließ sich nicht anpassen: '.$fehler->getMessage(), $schritte);
|
||||
}
|
||||
$this->protokoll('ok Migrationen: '.$schritte[0]['detail']);
|
||||
|
||||
if (null !== $pruefen) {
|
||||
$this->phase('pruefen', 'Die neue Fassung wird geprüft …');
|
||||
try {
|
||||
$problem = $pruefen();
|
||||
} catch (\Throwable $fehler) {
|
||||
$problem = $fehler->getMessage();
|
||||
}
|
||||
if (null !== $problem) {
|
||||
$schritte[] = ['step' => 'Funktion prüfen', 'ok' => false, 'detail' => $problem];
|
||||
$this->protokoll('FEHLER Funktion prüfen: '.$problem);
|
||||
|
||||
return $this->zurueckschalten($offen, 'Die neue Fassung lief nicht: '.$problem, $schritte);
|
||||
}
|
||||
$schritte[] = ['step' => 'Funktion prüfen', 'ok' => true, 'detail' => 'Startseite und Datenbank antworten.'];
|
||||
$this->protokoll('ok Funktion prüfen: Startseite und Datenbank antworten.');
|
||||
}
|
||||
|
||||
@unlink($this->root.'/'.self::MARKE);
|
||||
$this->updater->maintenance(false);
|
||||
$this->protokoll('ok Migrationen: '.$schritte[0]['detail']);
|
||||
foreach (self::ENTFALLEN_IN_PUBLIC as $eintrag) {
|
||||
@unlink($this->root.'/public/'.$eintrag);
|
||||
}
|
||||
@@ -329,10 +388,109 @@ final class ArchiveUpdate
|
||||
'Fassung %s läuft. Der vorherige Stand liegt unter var/update/ (die zwei jüngsten bleiben).',
|
||||
$this->updater->version(),
|
||||
)];
|
||||
$this->phase('fertig', sprintf('Fassung %s läuft.', $this->updater->version()), ['laeuft' => false]);
|
||||
|
||||
return ['ok' => true, 'steps' => $schritte];
|
||||
}
|
||||
|
||||
/**
|
||||
* Zurück auf die Fassung von vorher: Code aus var/update/<lauf>/alt, die überschriebenen
|
||||
* Dateien in public/ und plugins/, dann die Datenbank aus der Sicherung vor dem Update.
|
||||
* Erst danach geht die Wartungsseite aus - eine halb zurückgeschaltete Website darf niemand
|
||||
* sehen.
|
||||
*/
|
||||
private function zurueckschalten(?array $offen, string $grund, array $schritte): array
|
||||
{
|
||||
$bei = $this->status()['phase'] ?? null;
|
||||
$this->phase('zurueckschalten', 'Etwas ist schiefgegangen – die bisherige Fassung wird wiederhergestellt …', ['bei' => in_array($bei, self::PHASEN, true) ? $bei : null]);
|
||||
$arbeit = $this->root.'/var/update/'.basename((string) ($offen['alt'] ?? ''));
|
||||
$fehler = [];
|
||||
if (null === $offen || !is_dir($arbeit.'/alt')) {
|
||||
$fehler[] = 'Der beiseitegelegte Stand fehlt.';
|
||||
} else {
|
||||
@mkdir($arbeit.'/verworfen', 0o775, true);
|
||||
foreach (self::ERSETZEN as $eintrag) {
|
||||
if (!file_exists($arbeit.'/alt/'.$eintrag)) {
|
||||
continue;
|
||||
}
|
||||
if (file_exists($this->root.'/'.$eintrag) && !@rename($this->root.'/'.$eintrag, $arbeit.'/verworfen/'.$eintrag)) {
|
||||
$fehler[] = $eintrag.' ließ sich nicht beiseitelegen.';
|
||||
continue;
|
||||
}
|
||||
if (!@rename($arbeit.'/alt/'.$eintrag, $this->root.'/'.$eintrag)) {
|
||||
$fehler[] = $eintrag.' ließ sich nicht zurücklegen.';
|
||||
}
|
||||
}
|
||||
$liste = json_decode((string) @file_get_contents($arbeit.'/public.json'), true);
|
||||
foreach ((array) ($liste['neu'] ?? []) as $relativ) {
|
||||
@unlink($this->root.'/'.$relativ);
|
||||
}
|
||||
foreach ((array) ($liste['alt'] ?? []) as $relativ) {
|
||||
if (!@copy($arbeit.'/alt-public/'.$relativ, $this->root.'/'.$relativ)) {
|
||||
$fehler[] = $relativ.' ließ sich nicht zurückkopieren.';
|
||||
}
|
||||
}
|
||||
}
|
||||
if ('' !== (string) ($offen['sicherung'] ?? '')) {
|
||||
$wieder = $this->backup->restore((string) $offen['sicherung']);
|
||||
if (!$wieder['ok']) {
|
||||
$fehler[] = 'Die Datenbank ließ sich nicht aus der Sicherung zurückholen.';
|
||||
}
|
||||
}
|
||||
$von = (string) ($offen['von'] ?? '');
|
||||
if ([] === $fehler) {
|
||||
@unlink($this->root.'/'.self::MARKE);
|
||||
$this->updater->maintenance(false);
|
||||
$meldung = sprintf('%s Zurückgeschaltet auf %s – die Website läuft wie vorher.', $grund, '' === $von ? 'die vorherige Fassung' : $von);
|
||||
$schritte[] = ['step' => 'Zurückgeschaltet', 'ok' => true, 'detail' => $meldung];
|
||||
$this->protokoll('ok Zurückgeschaltet: '.$meldung);
|
||||
$this->phase('zurueckgerollt', $meldung, ['laeuft' => false]);
|
||||
} else {
|
||||
// Bleibt die Wartungsseite an, sieht niemand eine halbe Website; der Rest ist Handarbeit.
|
||||
$meldung = $grund.' Das Zurückschalten gelang nicht vollständig: '.implode(' ', $fehler).' Die Wartungsseite bleibt an.';
|
||||
$schritte[] = ['step' => 'Zurückschalten', 'ok' => false, 'detail' => $meldung];
|
||||
$this->protokoll('FEHLER Zurückschalten: '.$meldung);
|
||||
$this->phase('fehler', $meldung, ['laeuft' => false]);
|
||||
}
|
||||
|
||||
return ['ok' => false, 'steps' => $schritte, 'zurueckgeschaltet' => [] === $fehler];
|
||||
}
|
||||
|
||||
/** Der letzte Fortschritt, oder null, wenn noch nie ein Update über diesen Weg lief. */
|
||||
public function status(): ?array
|
||||
{
|
||||
$daten = json_decode((string) @file_get_contents($this->root.'/'.self::STATUS), true);
|
||||
if (!is_array($daten)) {
|
||||
return null;
|
||||
}
|
||||
// Ein Lauf, der seit einer halben Stunde nichts mehr meldet, ist abgebrochen - etwa weil
|
||||
// das Hosting den Aufruf beendet hat. Dann soll die Seite nicht ewig „läuft“ zeigen.
|
||||
if (($daten['laeuft'] ?? false) && strtotime((string) ($daten['aktualisiert'] ?? '')) < time() - 1800) {
|
||||
$daten['laeuft'] = false;
|
||||
$daten['phase'] = 'fehler';
|
||||
$daten['meldung'] = 'Das Update hat seit einer halben Stunde nichts mehr gemeldet und gilt als abgebrochen. Den Stand zeigt das Protokoll unten.';
|
||||
}
|
||||
|
||||
return $daten;
|
||||
}
|
||||
|
||||
/** Schreibt den Fortschritt - in einem Zug, damit die Seite nie eine halbe Datei liest. */
|
||||
private function phase(string $phase, string $meldung, array $mehr = [], bool $neu = false): void
|
||||
{
|
||||
$daten = $neu ? [] : ($this->status() ?? []);
|
||||
// Bei einem Fehler zählt, in welchem Schritt er passierte - die Seite markiert genau den.
|
||||
if (in_array($phase, ['fehler', 'zurueckgerollt'], true) && !array_key_exists('bei', $mehr)) {
|
||||
$mehr['bei'] = in_array($daten['phase'] ?? null, self::PHASEN, true) ? $daten['phase'] : ($daten['bei'] ?? null);
|
||||
}
|
||||
$daten = [...$daten, ...$mehr, 'phase' => $phase, 'meldung' => $meldung, 'aktualisiert' => gmdate('c')];
|
||||
$datei = $this->root.'/'.self::STATUS;
|
||||
@mkdir(dirname($datei), 0o775, true);
|
||||
$zwischen = $datei.'.'.bin2hex(random_bytes(4)).'.tmp';
|
||||
if (false !== @file_put_contents($zwischen, json_encode($daten, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))) {
|
||||
@rename($zwischen, $datei);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Zeile ins Protokoll, und zwar sofort.
|
||||
*
|
||||
@@ -366,6 +524,7 @@ final class ArchiveUpdate
|
||||
|
||||
continue;
|
||||
}
|
||||
$this->merken($ziel);
|
||||
if ('.htaccess' === $eintrag && is_file($ziel)) {
|
||||
$this->mergeHtaccess($quelle, $ziel);
|
||||
|
||||
@@ -375,6 +534,25 @@ final class ArchiveUpdate
|
||||
}
|
||||
}
|
||||
|
||||
/** Merkt sich eine Datei, bevor sie überschrieben oder neu angelegt wird. */
|
||||
private function merken(string $ziel): void
|
||||
{
|
||||
if (null === $this->publicSicherung) {
|
||||
return;
|
||||
}
|
||||
$relativ = ltrim(substr($ziel, strlen($this->root)), '/');
|
||||
if (!is_file($ziel)) {
|
||||
$this->publicNeu[] = $relativ;
|
||||
|
||||
return;
|
||||
}
|
||||
$kopie = $this->publicSicherung.'/'.$relativ;
|
||||
@mkdir(dirname($kopie), 0o775, true);
|
||||
if (@copy($ziel, $kopie)) {
|
||||
$this->publicAlt[] = $relativ;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Die neue .htaccess uebernehmen, aber die Zeilen, die der Betreiber eingeschaltet hat,
|
||||
* eingeschaltet lassen: HTTPS-Zwang und HSTS. Vorher kam jedes Update mit den auskommentierten
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ops;
|
||||
|
||||
/**
|
||||
* Die letzte Sicherung, wenn die Anwendung nicht einmal mehr eine Fehlerseite zustande bringt
|
||||
* (ADR 113).
|
||||
*
|
||||
* Kantarci Reisen am 09.10.2026: Datenbank per FTP ersetzt, danach antwortete jede Adresse mit
|
||||
* einem leeren 500er - für Besucher eine weiße Seite, für den Betreiber kein Hinweis, warum. Der
|
||||
* Grund stand nur im Fehlerprotokoll des Hostings.
|
||||
*
|
||||
* Diese Klasse hängt an nichts aus dem Framework: kein Container, keine Datenbank, keine
|
||||
* Vorlage. Sie schreibt den Grund nach var/log/notfall.log (per FTP lesbar, außerhalb von
|
||||
* public/) und zeigt Besuchern eine schlichte Wartungsseite mit 503 - für Suchmaschinen heißt das
|
||||
* „gleich wieder da“, nicht „kaputt“. Details stehen nie auf der Seite.
|
||||
*/
|
||||
final class Notfall
|
||||
{
|
||||
public const PROTOKOLL = 'var/log/notfall.log';
|
||||
/** Größer wird die Datei nicht; die ältere Hälfte wandert nach notfall.log.1. */
|
||||
private const GRENZE = 1048576;
|
||||
/** Fehlerarten, nach denen PHP abbricht - nur die gehören auf die Notfallseite. */
|
||||
private const ABBRUCH = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR];
|
||||
|
||||
public const KOPFZEILEN = [
|
||||
'Content-Type' => 'text/html; charset=UTF-8',
|
||||
'Retry-After' => '300',
|
||||
'Cache-Control' => 'no-store',
|
||||
'X-Robots-Tag' => 'noindex',
|
||||
'Content-Security-Policy' => "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'",
|
||||
];
|
||||
|
||||
private static ?string $wurzel = null;
|
||||
private static bool $gezeigt = false;
|
||||
|
||||
/** Einmal ganz am Anfang von public/index.php. */
|
||||
public static function einschalten(string $wurzel): void
|
||||
{
|
||||
self::$wurzel = $wurzel;
|
||||
register_shutdown_function(static function (): void {
|
||||
$fehler = error_get_last();
|
||||
if (null === $fehler || !in_array($fehler['type'], self::ABBRUCH, true)) {
|
||||
return;
|
||||
}
|
||||
self::melden(sprintf('PHP-Abbruch: %s in %s:%d', $fehler['message'], $fehler['file'], $fehler['line']));
|
||||
self::seite();
|
||||
// Danach soll nichts mehr ausgeben - auch nicht die Fehlerseite des Frameworks, das
|
||||
// sich ebenfalls fürs Herunterfahren angemeldet hat. exit in einer Shutdown-Funktion
|
||||
// beendet die übrigen.
|
||||
exit;
|
||||
});
|
||||
}
|
||||
|
||||
/** Eine Ausnahme, die aus dem Framework herausgefallen ist. */
|
||||
public static function ausnahme(\Throwable $fehler, string $adresse = ''): void
|
||||
{
|
||||
self::melden(self::beschreiben($fehler, $adresse));
|
||||
self::seite();
|
||||
}
|
||||
|
||||
/** Ein Fehler, den das Framework abgefangen und als 500 beantwortet hat - nur protokollieren. */
|
||||
public static function protokollieren(\Throwable $fehler, string $adresse = ''): void
|
||||
{
|
||||
self::melden(self::beschreiben($fehler, $adresse));
|
||||
}
|
||||
|
||||
public static function beschreiben(\Throwable $fehler, string $adresse = ''): string
|
||||
{
|
||||
$zeile = sprintf('%s: %s in %s:%d', $fehler::class, $fehler->getMessage(), $fehler->getFile(), $fehler->getLine());
|
||||
// Die eigentliche Ursache steckt oft eine Ebene tiefer, etwa das SQLite-Problem unter
|
||||
// einer Doctrine-Ausnahme.
|
||||
for ($tiefer = $fehler->getPrevious(), $n = 0; null !== $tiefer && $n < 3; $tiefer = $tiefer->getPrevious(), ++$n) {
|
||||
$zeile .= sprintf(' | Ursache: %s: %s', $tiefer::class, $tiefer->getMessage());
|
||||
}
|
||||
|
||||
return ('' === $adresse ? '' : $adresse.' – ').$zeile;
|
||||
}
|
||||
|
||||
private static function melden(string $text): void
|
||||
{
|
||||
$wurzel = self::$wurzel ?? dirname(__DIR__, 2);
|
||||
$datei = $wurzel.'/'.self::PROTOKOLL;
|
||||
if (!is_dir(dirname($datei))) {
|
||||
@mkdir(dirname($datei), 0o775, true);
|
||||
}
|
||||
if (is_file($datei) && filesize($datei) > self::GRENZE) {
|
||||
@rename($datei, $datei.'.1');
|
||||
}
|
||||
// Zeilenumbrüche aus Meldungen würden das Protokoll zerreißen; eine Zeile je Ereignis.
|
||||
$text = str_replace(["\r", "\n"], ' ', $text);
|
||||
@file_put_contents($datei, sprintf("[%s UTC] %s\n", gmdate('Y-m-d H:i:s'), mb_substr($text, 0, 4000)), FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
/** Die Seite für Besucher. Ohne Details, ohne Abhängigkeiten, ohne fremde Quellen. */
|
||||
private static function seite(): void
|
||||
{
|
||||
if (self::$gezeigt || PHP_SAPI === 'cli') {
|
||||
return;
|
||||
}
|
||||
self::$gezeigt = true;
|
||||
while (ob_get_level() > 0) {
|
||||
@ob_end_clean();
|
||||
}
|
||||
if (!headers_sent()) {
|
||||
http_response_code(503);
|
||||
foreach (self::KOPFZEILEN as $name => $wert) {
|
||||
header($name.': '.$wert);
|
||||
}
|
||||
}
|
||||
echo self::html();
|
||||
}
|
||||
|
||||
/** Die Wartungsseite als Text, auch für den Fehler-Listener im Framework. */
|
||||
public static function html(): string
|
||||
{
|
||||
return '<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">'
|
||||
.'<title>Gleich wieder da</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;font:17px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif;background:#f4f6f5;color:#1f2d27}'
|
||||
.'main{max-width:460px;padding:32px;text-align:center}h1{font-size:26px;line-height:1.25;margin:0 0 12px}p{margin:0;color:#46514c}</style></head>'
|
||||
.'<body><main><h1>Die Website ist gleich wieder da.</h1><p>Gerade wird etwas erneuert. Bitte versuche es in ein paar Minuten noch einmal.</p>'
|
||||
.'<p lang="en" style="margin-top:14px;font-size:14px">This website will be back shortly.</p></main></body></html>';
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user