Auf dem Hosting brach die Installation im dritten Schritt ab: near
"(": syntax error. Dort laeuft SQLite in einer Fassung vor 3.16, und
Doctrine liest das Schema mit Tabellenfunktionen wie
pragma_table_info(...), die es erst seit 3.16 gibt.
Die Pruefung nennt die gefundene Fassung jetzt vor der Wahl der
Datenbank, und der Datenbankschritt bietet SQLite gar nicht erst an,
wenn sie zu alt ist - mit dem Hinweis auf MySQL. Eine Voraussetzung,
die erst mitten in der Installation auffaellt, ist keine Pruefung wert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
214 lines
8.3 KiB
PHP
214 lines
8.3 KiB
PHP
<?php
|
|
|
|
namespace App\Controller;
|
|
|
|
use App\Domain\InvalidPage;
|
|
use App\Ops\Installation;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
|
|
/**
|
|
* Six screens from an empty webspace to a website, without a console.
|
|
*
|
|
* Every screen answers only while the installation is unfinished, and every one of them writes its
|
|
* result straight away, so a reload continues where it stopped instead of starting over. The order
|
|
* is fixed and enforced here, not only suggested by links: a step whose requirement is missing
|
|
* sends you back to the step that produces it.
|
|
*
|
|
* After the first account exists, every route in this file answers 404 - not because a file was
|
|
* deleted, but because the database says there is an installation.
|
|
*/
|
|
final class InstallController extends AbstractController
|
|
{
|
|
public function __construct(private Installation $installation)
|
|
{
|
|
}
|
|
|
|
#[Route('/install', name: 'install', methods: ['GET'], priority: 10)]
|
|
public function check(): Response
|
|
{
|
|
$this->guard();
|
|
$checks = $this->installation->checks();
|
|
|
|
return $this->render('install/check.html.twig', [
|
|
'step' => 1,
|
|
'checks' => $checks,
|
|
'ready' => $this->installation->ready($checks),
|
|
]);
|
|
}
|
|
|
|
#[Route('/install/datenbank', name: 'install_database', methods: ['GET', 'POST'], priority: 10)]
|
|
public function database(Request $request): Response
|
|
{
|
|
$this->guard();
|
|
$error = null;
|
|
$driver = $request->request->getString('driver', 'sqlite');
|
|
if ($request->isMethod('POST') && $this->signed($request, 'datenbank')) {
|
|
$input = [
|
|
'host' => $request->request->getString('host', '127.0.0.1'),
|
|
'port' => $request->request->getString('port', '3306'),
|
|
'name' => $request->request->getString('name', 'cms'),
|
|
'user' => $request->request->getString('user'),
|
|
'password' => $request->request->getString('password'),
|
|
];
|
|
$result = $this->installation->testDatabase('mysql' === $driver ? 'mysql' : 'sqlite', $input);
|
|
$error = $result['error'];
|
|
if (null === $error) {
|
|
try {
|
|
$this->installation->writeEnvironment(['APP_ENV' => 'prod', 'APP_DEBUG' => '0', 'DATABASE_URL' => $result['url']]);
|
|
|
|
// The new connection only exists in the next request, because the environment
|
|
// file is read while booting. That is also why this is a redirect and not a
|
|
// rendered page: the migration screen has to talk to the new database.
|
|
return $this->redirectToRoute('install_migration');
|
|
} catch (\RuntimeException $e) {
|
|
$error = $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->render('install/database.html.twig', [
|
|
'step' => 2,
|
|
'driver' => $driver,
|
|
'error' => $error,
|
|
'values' => $request->request->all(),
|
|
'mysql' => \extension_loaded('pdo_mysql'),
|
|
// SQLite nur, wenn die Fassung die Tabellenfunktionen kennt, mit denen Doctrine das
|
|
// Schema liest - sonst scheitert erst der naechste Schritt, und zwar unverstaendlich.
|
|
'sqlite' => \extension_loaded('pdo_sqlite') && null !== $this->installation->sqliteVersion()
|
|
&& version_compare((string) $this->installation->sqliteVersion(), Installation::MIN_SQLITE, '>='),
|
|
'sqliteVersion' => $this->installation->sqliteVersion(),
|
|
'minSqlite' => Installation::MIN_SQLITE,
|
|
...$this->token('datenbank'),
|
|
]);
|
|
}
|
|
|
|
#[Route('/install/migration', name: 'install_migration', methods: ['GET', 'POST'], priority: 10)]
|
|
public function migration(Request $request): Response
|
|
{
|
|
$this->guard();
|
|
if (!$this->installation->environmentWritten()) {
|
|
return $this->redirectToRoute('install_database');
|
|
}
|
|
$error = null;
|
|
$applied = null;
|
|
if ($request->isMethod('POST') && $this->signed($request, 'migration')) {
|
|
@set_time_limit(0);
|
|
try {
|
|
$applied = $this->installation->migrate();
|
|
|
|
return $this->redirectToRoute('install_website');
|
|
} catch (\Throwable $e) {
|
|
$error = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
return $this->render('install/migration.html.twig', [
|
|
'step' => 3,
|
|
'pending' => $error ? [] : $this->pending(),
|
|
'applied' => $applied,
|
|
'error' => $error,
|
|
...$this->token('migration'),
|
|
]);
|
|
}
|
|
|
|
#[Route('/install/website', name: 'install_website', methods: ['GET', 'POST'], priority: 10)]
|
|
public function website(Request $request): Response
|
|
{
|
|
$this->guard();
|
|
if (!$this->installation->schemaReady()) {
|
|
return $this->redirectToRoute('install_migration');
|
|
}
|
|
$error = null;
|
|
if ($request->isMethod('POST') && $this->signed($request, 'website')) {
|
|
try {
|
|
$this->installation->createSite(
|
|
$request->request->getString('name'),
|
|
$request->request->getString('origin'),
|
|
$request->request->getString('locale', 'de'),
|
|
);
|
|
|
|
return $this->redirectToRoute('install_account');
|
|
} catch (InvalidPage|\RuntimeException $e) {
|
|
$error = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
return $this->render('install/website.html.twig', [
|
|
'step' => 4,
|
|
'error' => $error,
|
|
'values' => $request->request->all(),
|
|
// The address the browser is on is almost always the right one, so it is prefilled.
|
|
'origin' => $request->getSchemeAndHttpHost(),
|
|
...$this->token('website'),
|
|
]);
|
|
}
|
|
|
|
#[Route('/install/konto', name: 'install_account', methods: ['GET', 'POST'], priority: 10)]
|
|
public function account(Request $request): Response
|
|
{
|
|
$this->guard();
|
|
if (!$this->installation->siteReady()) {
|
|
return $this->redirectToRoute('install_website');
|
|
}
|
|
$error = null;
|
|
if ($request->isMethod('POST') && $this->signed($request, 'konto')) {
|
|
try {
|
|
$this->installation->createAdmin(
|
|
$request->request->getString('username'),
|
|
$request->request->getString('password'),
|
|
$request->request->getString('repeat'),
|
|
);
|
|
|
|
// The last screen is rendered here, not after a redirect: one request later this
|
|
// route is gone, because the account it just created ends the installation.
|
|
return $this->render('install/done.html.twig', [
|
|
'step' => 6,
|
|
'username' => strtolower(trim($request->request->getString('username'))),
|
|
]);
|
|
} catch (InvalidPage $e) {
|
|
$error = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
return $this->render('install/account.html.twig', [
|
|
'step' => 5,
|
|
'error' => $error,
|
|
'username' => $request->request->getString('username'),
|
|
...$this->token('konto'),
|
|
]);
|
|
}
|
|
|
|
/** Reading the pending list must never be the reason the screen cannot be shown. */
|
|
private function pending(): array
|
|
{
|
|
try {
|
|
return $this->installation->pendingMigrations();
|
|
} catch (\Throwable) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private function guard(): void
|
|
{
|
|
if ($this->installation->isInstalled()) {
|
|
throw new NotFoundHttpException('Diese Installation ist abgeschlossen.');
|
|
}
|
|
}
|
|
|
|
private function token(string $step): array
|
|
{
|
|
$issued = time();
|
|
|
|
return ['issued' => $issued, 'signature' => $this->installation->sign($step, $issued)];
|
|
}
|
|
|
|
private function signed(Request $request, string $step): bool
|
|
{
|
|
return $this->installation->verify($step, $request->request->getInt('issued'), $request->request->getString('signature'));
|
|
}
|
|
}
|