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>
105 lines
4.9 KiB
PHP
105 lines
4.9 KiB
PHP
<?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');
|
||
}
|
||
}
|