Fassung 1.4.4: Migrationen im Browser, und ein Fehlschlag sagt warum
Zwei Luecken, die erst das Veroeffentlichen von 1.4.3 gezeigt hat. - Es gab keinen Weg, ausstehende Migrationen im Browser auszufuehren. Wer die Dateien per FTP hochlaedt - und das muss genau einmal jeder tun, dessen Installation die Updatekarte noch nicht kennt -, hatte danach neuen Code und eine alte Datenbank, und dafuer nur die Konsole. Also genau das, was auf geteiltem Hosting fehlt. Der Bildschirm "System" zeigt jetzt eine Karte, sobald etwas aussteht, nennt die Namen und fuehrt sie auf Knopfdruck aus. - "Die Releaseseite antwortet nicht" waren drei Ursachen in einem Satz. Beim Gegenpruefen des frischen Releases holte der Updater von hier gar nichts - nicht weil die Leitung zu waere, sondern weil diesem PHP der Zertifikatsspeicher fehlt. Auf einem Webhosting ist das der haeufigste Grund und der einzige, den man in einer Minute behebt; er steht jetzt in der Meldung. Bei genau diesem Fehler wird zusaetzlich der Weg ueber den Dateizeiger versucht - damit funktioniert der Abruf auf diesem Rechner. Abgeschaltet wird die Pruefung nicht: ein Update ohne Zertifikatspruefung koennte unterwegs ausgetauscht worden sein. Dazu der manuelle Weg Schritt fuer Schritt in docs/UPDATE.md und ein Test, der beides festhaelt: dass die Karte auffaellt und dass nur ein angemeldeter Administrator mit gueltigem Formular migrieren kann. Geprueft: 431 Tests auf PHP 8.2 und 8.4, Editor-, Browser- und Linkpruefung, Editor-Modelltests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d6d476177a
commit
4153ba0f65
10 files changed
+304
-7
No files matched your search
@@ -44,6 +44,10 @@ final class SystemController extends AbstractController
|
||||
// Der Weg ohne Git: das Releasearchiv. Auf geteiltem Hosting ist das der einzige.
|
||||
'archiveVersion' => $request->query->getBoolean('pruefen') ? $this->archive->updateAvailable() : null,
|
||||
'archivePending' => $this->archive->pending(),
|
||||
// Wer die Dateien von Hand hochgeladen hat - über FTP, ohne Git und ohne das
|
||||
// Releasearchiv -, hat danach neuen Code und eine alte Datenbank. Dieser Bildschirm
|
||||
// ist die einzige Stelle, an der das jemand merken kann.
|
||||
'pendingMigrations' => $this->pendingMigrations(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -54,6 +58,44 @@ final class SystemController extends AbstractController
|
||||
* Migrationen aus. Dazwischen liegt eine neue Anfrage, denn erst die startet mit dem Code der
|
||||
* neuen Fassung; ein Prozess, der sich selbst ausgetauscht hat, arbeitet mit dem alten weiter.
|
||||
*/
|
||||
/**
|
||||
* Ausstehende Migrationen ausführen - ohne Git, ohne Archiv, ohne Konsole.
|
||||
*
|
||||
* Der Weg für ein Update, das von Hand eingespielt wurde: Dateien liegen neu, die Datenbank
|
||||
* ist alt. Ohne diesen Knopf bräuchte es eine Konsole, und genau die gibt es auf geteiltem
|
||||
* Hosting nicht.
|
||||
*/
|
||||
#[Route('/%kant.admin_path%/system/migrationen', name: 'admin_system_migrate', methods: ['POST'])]
|
||||
public function migrate(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
if (!$this->isCsrfTokenValid('system', $request->request->getString('_csrf_token'))) {
|
||||
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||||
}
|
||||
@set_time_limit(0);
|
||||
|
||||
try {
|
||||
$namen = $this->installation->migrate();
|
||||
$this->addFlash('success', [] === $namen
|
||||
? 'Es stand keine Migration an - die Datenbank war bereits auf dem Stand des Codes.'
|
||||
: sprintf('%d Migration(en) ausgeführt: %s.', count($namen), implode(', ', $namen)));
|
||||
} catch (\Throwable $fehler) {
|
||||
$this->addFlash('error', 'Die Migration ist fehlgeschlagen: '.$fehler->getMessage());
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_system');
|
||||
}
|
||||
|
||||
/** Das Lesen der Liste darf nie der Grund sein, dass dieser Bildschirm nicht erscheint. */
|
||||
private function pendingMigrations(): array
|
||||
{
|
||||
try {
|
||||
return $this->installation->pendingMigrations();
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/%kant.admin_path%/system/archiv', name: 'admin_system_archive', methods: ['POST'])]
|
||||
public function archive(Request $request): Response
|
||||
{
|
||||
|
||||
@@ -104,7 +104,7 @@ final class ArchiveUpdate
|
||||
|
||||
$neueste = $this->latest();
|
||||
if (null === $neueste) {
|
||||
$melden($schritte, 'Fassung ermitteln', false, 'Die Releaseseite antwortet nicht. Ohne sie ist nicht feststellbar, was aktuell ist.');
|
||||
$melden($schritte, 'Fassung ermitteln', false, trim('Die Releaseseite antwortet nicht. Ohne sie ist nicht feststellbar, was aktuell ist. '.$this->download->reason()));
|
||||
|
||||
return ['ok' => false, 'steps' => $schritte];
|
||||
}
|
||||
@@ -136,7 +136,7 @@ final class ArchiveUpdate
|
||||
$archiv = $arbeit.'/kantsites-'.$ziel.'.zip';
|
||||
$inhalt = $this->download->get(sprintf('%s/v%s/kantsites-%s.zip', $this->releaseBase, $ziel, $ziel));
|
||||
if (null === $inhalt || '' === $inhalt) {
|
||||
$melden($schritte, 'Archiv laden', false, 'Das Archiv ließ sich nicht herunterladen. Darf dieser Server nach außen verbinden?');
|
||||
$melden($schritte, 'Archiv laden', false, trim('Das Archiv ließ sich nicht herunterladen. '.$this->download->reason()));
|
||||
|
||||
return ['ok' => false, 'steps' => $schritte];
|
||||
}
|
||||
|
||||
+57
-3
@@ -15,18 +15,46 @@ namespace App\Ops;
|
||||
*/
|
||||
class Download
|
||||
{
|
||||
/**
|
||||
* Die curl-Fehler, die „ich kann das Zertifikat nicht prüfen" heißen.
|
||||
*
|
||||
* Als Zahlen und nicht als Konstanten: `CURLE_PEER_FAILED_VERIFICATION` gibt es nicht in
|
||||
* jedem PHP-Bau, und ein fehlender Konstantenname wäre ein Absturz genau an der Stelle, an
|
||||
* der eigentlich eine hilfreiche Meldung stehen soll.
|
||||
*/
|
||||
private const ZERTIFIKATSFEHLER = [51, 60, 77];
|
||||
|
||||
/** Warum der letzte Versuch nichts gebracht hat - in einem Satz, der jemandem weiterhilft. */
|
||||
private string $grund = '';
|
||||
|
||||
public function __construct(
|
||||
private int $connectTimeout = 15,
|
||||
private int $timeout = 900,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Grund des letzten Fehlschlags.
|
||||
*
|
||||
* Ohne ihn steht in der Verwaltung „die Releaseseite antwortet nicht", und das ist in drei
|
||||
* Fällen dieselbe Meldung: kein Weg nach draußen, kein Zertifikatsspeicher, falsche Adresse.
|
||||
* Der mittlere ist auf einem Webhosting der häufigste und der einzige, den man in einer
|
||||
* Minute behebt - er muss also dastehen.
|
||||
*/
|
||||
public function reason(): string
|
||||
{
|
||||
return $this->grund;
|
||||
}
|
||||
|
||||
/** Den Inhalt der Adresse, oder null wenn sie nicht mit 200 antwortet. */
|
||||
public function get(string $url): ?string
|
||||
{
|
||||
$this->grund = '';
|
||||
// Kein http, kein file, kein ftp: ein Update, das über eine unverschlüsselte Verbindung
|
||||
// kommt, kann unterwegs ausgetauscht worden sein.
|
||||
if (!str_starts_with($url, 'https://')) {
|
||||
$this->grund = 'Nur HTTPS-Adressen werden geholt.';
|
||||
|
||||
return null;
|
||||
}
|
||||
if (function_exists('curl_init')) {
|
||||
@@ -43,11 +71,29 @@ class Download
|
||||
]);
|
||||
$antwort = curl_exec($curl);
|
||||
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
$fehler = curl_errno($curl);
|
||||
$text = curl_error($curl);
|
||||
curl_close($curl);
|
||||
|
||||
return is_string($antwort) && 200 === $status ? $antwort : null;
|
||||
if (is_string($antwort) && 200 === $status) {
|
||||
return $antwort;
|
||||
}
|
||||
// Ein fehlender Zertifikatsspeicher ist keine kaputte Leitung, sondern eine
|
||||
// Einstellung - und er sieht sonst genauso aus wie eine gesperrte Verbindung.
|
||||
$this->grund = match (true) {
|
||||
in_array($fehler, self::ZERTIFIKATSFEHLER, true) => 'Das Zertifikat der Gegenstelle lässt sich nicht prüfen: Diesem PHP fehlt ein Zertifikatsspeicher (curl.cainfo in der php.ini). Geholt wird trotzdem nur mit Prüfung - ein Update ohne sie könnte unterwegs ausgetauscht worden sein.',
|
||||
0 !== $fehler => 'Die Verbindung kam nicht zustande: '.$text,
|
||||
0 === $status => 'Der Server hat nicht geantwortet.',
|
||||
default => sprintf('Die Adresse antwortet mit %d statt 200.', $status),
|
||||
};
|
||||
// Bei einem Zertifikatsproblem lohnt der zweite Weg: Der Dateizeiger benutzt die
|
||||
// Einstellungen von OpenSSL, und die sind manchmal gesetzt, wo curl nichts findet.
|
||||
if (!in_array($fehler, self::ZERTIFIKATSFEHLER, true)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (!filter_var(ini_get('allow_url_fopen'), FILTER_VALIDATE_BOOL)) {
|
||||
$this->grund = '' !== $this->grund ? $this->grund : 'Dieser Server darf keine Adressen abrufen: curl fehlt und allow_url_fopen ist abgeschaltet.';
|
||||
|
||||
return null;
|
||||
}
|
||||
$context = stream_context_create([
|
||||
@@ -55,7 +101,15 @@ class Download
|
||||
'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
|
||||
]);
|
||||
$antwort = @file_get_contents($url, false, $context);
|
||||
if (is_string($antwort)) {
|
||||
$this->grund = '';
|
||||
|
||||
return is_string($antwort) ? $antwort : null;
|
||||
return $antwort;
|
||||
}
|
||||
if ('' === $this->grund) {
|
||||
$this->grund = 'Die Adresse war über den Dateizeiger nicht erreichbar.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user