Files
KantSites/src/Controller/PackageController.php
T
kantarciandClaude Opus 5.5 32e42f973b Move site content between installations as a package
System now exports a content package and imports one, by upload or from
var/pakete. Pages with their revisions, areas, menus, media, forms,
redirects, terms, translations, design and settings are replaced;
submissions, statistics, tasks, plugin state and service keys stay, and
accounts only come along when that is chosen explicitly. A backup comes
first, the site is in maintenance meanwhile, and every file and document
in the package is checked before anything is written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:31:06 +02:00

105 lines
4.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Controller;
use App\Content\FontLibrary;
use App\Content\Theme;
use App\Domain\InvalidPage;
use App\Ops\SitePackage;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Attribute\Route;
/**
* Inhaltspakete exportieren und importieren (ADR 118) - unter System, nur für Administratoren.
*
* Der Import ersetzt die Inhalte einer laufenden Website. Deshalb: CSRF, ein ausdrückliches
* Häkchen, vorher eine Sicherung, während des Imports Wartung. Konten werden nur übernommen,
* wenn man das eigens wählt - sonst bleiben Benutzer und Passwörter dieser Website.
*/
final class PackageController extends AbstractController
{
public function __construct(private SitePackage $packages, private FontLibrary $fonts, private Theme $theme)
{
}
#[Route('/%kant.admin_path%/system/paket/export', name: 'admin_package_export', methods: ['POST'])]
public function export(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 {
$file = $this->packages->export($request->request->getBoolean('benutzer'));
} catch (InvalidPage $e) {
return $this->back($e->getMessage());
}
$response = new BinaryFileResponse($file, headers: ['Content-Type' => 'application/zip', 'X-Content-Type-Options' => 'nosniff', 'Cache-Control' => 'no-store']);
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, basename($file));
// Ein Export ist zum Mitnehmen da, nicht zum Liegenlassen: er enthält alle Inhalte.
$response->deleteFileAfterSend();
return $response;
}
#[Route('/%kant.admin_path%/system/paket/import', name: 'admin_package_import', methods: ['POST'])]
public function import(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.');
}
if (!$request->request->getBoolean('bestaetigt')) {
return $this->back('Bitte bestätige, dass die Inhalte dieser Website ersetzt werden.');
}
$upload = $request->files->get('paket');
$waiting = basename($request->request->getString('wartend'));
$temporary = null;
if ($upload instanceof UploadedFile) {
if (!$upload->isValid()) {
return $this->back(\in_array($upload->getError(), [\UPLOAD_ERR_INI_SIZE, \UPLOAD_ERR_FORM_SIZE], true)
? 'Das Paket ist größer, als das Hosting für einen Upload erlaubt. Bitte per FTP nach var/pakete/ legen – dann erscheint es hier zur Auswahl.'
: 'Das Hochladen ist gescheitert. Bitte erneut versuchen oder das Paket per FTP nach var/pakete/ legen.');
}
$temporary = $this->packages->directory().'/hochgeladen-'.bin2hex(random_bytes(6)).'.zip';
@mkdir($this->packages->directory(), 0o775, true);
$upload->move(\dirname($temporary), basename($temporary));
$file = $temporary;
} elseif ('' !== $waiting && str_ends_with($waiting, '.zip') && is_file($this->packages->directory().'/'.$waiting)) {
$file = $this->packages->directory().'/'.$waiting;
} else {
return $this->back('Bitte ein Paket hochladen oder eines aus var/pakete/ wählen.');
}
@set_time_limit(0);
try {
$result = $this->packages->import($file, $request->request->getBoolean('benutzer'));
} finally {
if (null !== $temporary) {
@unlink($temporary);
}
}
// Das Ergebnis direkt, nicht über eine Weiterleitung: wurden die Konten ersetzt, gilt die
// Anmeldung ab der nächsten Anfrage nicht mehr.
// Schriften liegen nicht im Paket, sondern auf dem Server dieser Website (public/assets/fonts).
// Fehlt eine, die die neue Gestaltung verlangt, sagt die Ergebnisseite, wo man sie holt.
$missing = $result['ok'] ? $this->fonts->missing($this->theme->load()) : [];
return $this->render('admin/package-result.html.twig', ['result' => $result, 'fontsMissing' => $missing]);
}
private function back(string $message): Response
{
$this->addFlash('error', $message);
return $this->redirectToRoute('admin_system');
}
}