denyAccessUnlessGranted('ROLE_ADMIN'); $checks = $this->updater->check(); return $this->render('admin/system.html.twig', [ 'state' => $this->updater->state(), 'checks' => $checks, 'ready' => [] === array_filter($checks, static fn (array $check): bool => !$check['ok']), // Asking the remote costs a network round trip, so only when it is wanted. 'available' => $request->query->getBoolean('pruefen') ? $this->updater->availableVersion() : null, 'asked' => $request->query->getBoolean('pruefen'), 'steps' => $request->getSession()->remove('update_steps') ?: [], 'backups' => $this->backup->all(), 'adminPath' => $this->adminPath, // 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(), ]); } /** * Update über das Releasearchiv - für Installationen, die kein Git kennen. * * Zwei Schritte, und zwar mit Absicht: Der erste ersetzt die Dateien, der zweite führt die * 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 { $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); if ($request->request->getBoolean('finish')) { $ergebnis = $this->archive->finish($this->installation); $request->getSession()->set('update_steps', $ergebnis['steps']); $this->addFlash($ergebnis['ok'] ? 'success' : 'error', $ergebnis['ok'] ? 'Das Update ist fertig. Die Website ist wieder erreichbar.' : 'Die Migrationen sind fehlgeschlagen. Die Website bleibt im Wartungszustand.'); return $this->redirectToRoute('admin_system'); } $ergebnis = $this->archive->run($request->request->getString('target') ?: null); $request->getSession()->set('update_steps', $ergebnis['steps']); if ($ergebnis['migrate'] ?? false) { $this->addFlash('success', 'Die Dateien sind ersetzt. Jetzt noch die Migrationen - der Knopf steht oben.'); return $this->redirectToRoute('admin_system'); } $this->addFlash($ergebnis['ok'] ? 'success' : 'error', $ergebnis['ok'] ? 'Es war nichts einzuspielen.' : 'Das Update ist nicht durchgelaufen. Die Schritte stehen unten.'); return $this->redirectToRoute('admin_system'); } #[Route('/%kant.admin_path%/system/update', name: 'admin_system_update', methods: ['POST'])] public function update(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.'); } // Checkout, composer and migration take longer than a page usually may. Where the hosting // allows it, the limit is lifted; where it does not, the console is the way. @set_time_limit(0); $target = $request->request->getString('target'); $result = $this->updater->update('' === $target ? null : $target, $request->request->getBoolean('dryRun')); $request->getSession()->set('update_steps', $result['steps']); $this->addFlash( $result['ok'] ? 'success' : 'error', $result['ok'] ? sprintf('Update auf %s abgeschlossen.', $result['target'] ?? 'die aktuelle Fassung') : 'Das Update ist gescheitert. Die Schritte unten sagen, wo – und ob die Website wieder online ist.', ); return $this->redirectToRoute('admin_system'); } /** * Creating a backup is safe, so it has a button. Putting one back is not, so it has none - * `cms:restore` is the way, and the list says which backups exist and whether their checksum * still matches. */ #[Route('/%kant.admin_path%/system/sicherung', name: 'admin_system_backup', methods: ['POST'])] public function backup(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 { $result = $this->backup->create('Verwaltung'); } catch (InvalidPage $error) { return $this->back($error->getMessage()); } catch (\Throwable $error) { // Alles andere ist kein erwarteter Fall, sondern einer, den dieses Hosting mitbringt - // eine alte SQLite-Fassung, ein fehlendes Schreibrecht, ein voller Datentraeger. Eine // 500-Seite laesst den Betreiber damit allein; der Satz hier sagt wenigstens, woran es // lag. return $this->back('Die Sicherung ist fehlgeschlagen: '.$error->getMessage()); } $removed = $this->backup->rotate(); return $this->back(sprintf( '%s angelegt: %d Seite(n), %d Fassung(en), %d Mediendatei(en).%s', $result['name'], $result['manifest']['inhalt']['seiten'], $result['manifest']['inhalt']['fassungen'], $result['manifest']['medien']['dateien'], [] === $removed ? '' : ' Ältere entfernt: '.implode(', ', $removed).'.', ), 'success'); } #[Route('/%kant.admin_path%/system/wartung', name: 'admin_system_maintenance', methods: ['POST'])] public function maintenance(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.'); } $on = 'an' === $request->request->getString('state'); $this->updater->maintenance($on); return $this->back($on ? 'Der Wartungszustand ist eingeschaltet: die Website antwortet mit 503, die Verwaltung bleibt erreichbar.' : 'Der Wartungszustand ist aufgehoben.', 'success'); } private function back(string $message, string $type = 'error'): Response { $this->addFlash($type, $message); return $this->redirectToRoute('admin_system'); } }