22 private const TOTAL_STEPS = 7;
23 private const SESSION_SECTION =
'installer';
24 private const SESSION_MODULE =
'dbxSetup';
25 private const SQL_SERVER =
'dbxApp';
28 private array $steps = array(
29 1 => array(
'title' =>
'Systemprüfung',
'short' =>
'System',
'icon' =>
'bi-cpu'),
30 2 => array(
'title' =>
'Website & Design',
'short' =>
'Basis',
'icon' =>
'bi-palette'),
31 3 => array(
'title' =>
'Datenspeicher',
'short' =>
'Datenbank',
'icon' =>
'bi-database'),
32 4 => array(
'title' =>
'Strukturen & Daten',
'short' =>
'Synchronisierung',
'icon' =>
'bi-arrow-repeat'),
33 5 => array(
'title' =>
'Administration',
'short' =>
'Admin',
'icon' =>
'bi-shield-lock'),
34 6 => array(
'title' =>
'E-Mail',
'short' =>
'E-Mail',
'icon' =>
'bi-envelope-gear'),
35 7 => array(
'title' =>
'Prüfen & starten',
'short' =>
'Abschluss',
'icon' =>
'bi-rocket-takeoff'),
39 private array $errors = array();
42 private array $notices = array();
44 private bool $finished =
false;
46 private bool $stayOnStep =
false;
49 private array $finishResult = array();
53 dbx()->set_system_var(
'dbx_title',
'dbxapp installieren');
54 dbx()->set_system_var(
'dbx_meta_robots',
'noindex,nofollow,noarchive');
55 if (!headers_sent()) {
56 header(
'Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
57 header(
'Pragma: no-cache');
58 header(
'X-Robots-Tag: noindex, nofollow, noarchive');
65 public function run(): string
69 if ((
int)(
$config[
'install'] ?? 1) !== 1
70 && (
int)
dbx()->get_system_var(
'dbx_install', 0,
'int') !== 1
72 return $this->renderAlreadyInstalled();
74 $help = is_scalar(
$_GET[
'install_help'] ??
null)
75 ? strtolower(trim((
string)
$_GET[
'install_help']))
77 if (in_array(
$help, array(
'design',
'db3',
'pdo',
'email'),
true)) {
78 return $this->renderInstallerHelp(
$help);
81 $completed = max(0, min(
83 (
int)$this->state(
'completed', 0)
85 $step = $this->requestedStep();
86 $step = min($step, min(self::TOTAL_STEPS, $completed + 1));
88 if ($this->isPost()) {
89 if (!$this->validToken($step)) {
90 $this->errors[] =
'Die Installationssitzung ist abgelaufen. Bitte laden Sie die Seite neu.';
91 } elseif ($this->processStep($step)) {
92 if (!$this->stayOnStep) {
93 $this->setState(
'completed', max($completed, $step));
94 if (!$this->finished) {
95 $step = min(self::TOTAL_STEPS, $step + 1);
101 if ($this->finished) {
102 return $this->renderFinished();
105 return $this->renderShell($step, $this->renderStep($step));
116 $add =
static function (
128 'status' =>
$ok ?
'ok' : (
$required ?
'error' :
'warning'),
138 version_compare(PHP_VERSION,
'8.2.0',
'>='),
140 'dbxapp benötigt PHP 8.2 oder neuer.'
142 foreach (array(
'session',
'PDO',
'json',
'filter',
'hash',
'openssl') as $extension) {
144 'ext-' . strtolower($extension),
145 'PHP-Erweiterung ' . $extension,
146 extension_loaded($extension) ?
'verfügbar' :
'fehlt',
147 extension_loaded($extension),
149 'Die Erweiterung muss in php.ini aktiviert sein.'
153 $configDir =
dbx()->os_path(
156 $localConfigFile =
dbx()->os_path(
157 rtrim($configDir,
'/\\') . DIRECTORY_SEPARATOR .
'config.local.php'
159 $localConfigWritable = $this->directoryWritable($configDir)
160 && (!is_file($localConfigFile) || is_writable($localConfigFile));
163 'Lokale Konfiguration',
164 $this->compactPath($localConfigFile),
165 $localConfigWritable,
167 'Der Webserver benötigt Schreibrecht für config.local.php.'
172 ) as $name => $directory) {
173 $directory =
dbx()->os_path($directory);
176 'Schreibverzeichnis ' . $name .
'/',
177 $this->compactPath($directory),
178 $this->directoryWritable($directory),
180 'Das Verzeichnis wird für Laufzeitdaten benötigt.'
187 'Composer-Abhängigkeiten',
188 is_file($autoload) ?
'vollständig' :
'autoload.php fehlt',
189 is_file($autoload) && class_exists(
'\PHPMailer\PHPMailer\PHPMailer'),
191 'Vor der Installation muss composer install ausgeführt worden sein.'
195 'pdo_sqlite' =>
'SQLite/DB3',
196 'pdo_mysql' =>
'MySQL/MariaDB',
197 'pdo_pgsql' =>
'PostgreSQL',
198 'pdo_sqlsrv' =>
'Microsoft SQL Server',
199 'curl' =>
'Updates und externe APIs',
200 'gd' =>
'Bildverarbeitung',
201 'fileinfo' =>
'Dateityperkennung',
202 'zip' =>
'Updates und Archive',
203 'mbstring' =>
'erweiterte Unicode-Verarbeitung',
204 'intl' =>
'internationale Formate',
205 ) as $extension => $purpose) {
207 'optional-' . $extension,
208 'Optional: ' . $extension,
209 extension_loaded($extension) ?
'verfügbar' :
'nicht aktiv',
210 extension_loaded($extension),
212 'Empfohlen für ' . $purpose .
'.'
217 || (
int)(
$_SERVER[
'SERVER_PORT'] ?? 0) === 443
219 strtolower((
string)(
$_SERVER[
'HTTP_X_FORWARDED_PROTO'] ??
'')),
220 array(
'https',
'wss'),
223 $localHost = in_array(
224 strtolower((
string)(
$_SERVER[
'HTTP_HOST'] ??
$_SERVER[
'SERVER_NAME'] ??
'')),
225 array(
'localhost',
'127.0.0.1',
'[::1]'),
231 $https ?
'aktiv' : ($localHost ?
'lokale Entwicklung' :
'nicht erkannt'),
234 'Produktive Installationen sollten ausschließlich HTTPS verwenden.'
237 $memory = $this->iniBytes((
string)ini_get(
'memory_limit'));
241 (
string)ini_get(
'memory_limit'),
242 $memory === -1 || $memory >= 128 * 1024 * 1024,
244 'Für Installation und Updates werden mindestens 128 MB empfohlen.'
250 private function processStep(
int $step): bool
252 $action = $this->postKey(
'install_action', 40);
253 $this->stayOnStep = ($step === 3 && $action ===
'check_database')
254 || ($step === 6 && $action ===
'check_mail');
256 $processed = match ($step) {
257 1 => $this->processSystem(),
258 2 => $this->processBasics(),
259 3 => $this->processDatabase(),
260 4 => $this->processSchema(),
261 5 => $this->processAdmin(),
262 6 => $this->processMail(),
263 7 => $this->processFinish(),
267 if ($processed && $this->stayOnStep) {
268 $this->notices[] = $step === 3
269 ?
'Die Datenbankwerte wurden erneut geprüft. Sie bleiben in Schritt 3 und können die Installation anschließend fortsetzen.'
270 :
'Die E-Mail-Werte wurden erneut geprüft. Sie bleiben in Schritt 6 und können die Installation anschließend fortsetzen.';
276 private function processSystem(): bool
278 $failed = array_values(array_filter(
280 static fn(array
$check):
bool =>
284 $this->errors[] =
'Mindestens eine notwendige Systemvoraussetzung ist noch nicht erfüllt.';
288 $this->notices[] =
'Alle notwendigen Systemvoraussetzungen sind erfüllt.';
292 private function processBasics(): bool
294 $siteTitle = $this->postText(
'site_title', 120);
295 $brandName = $this->postText(
'brand_name', 120);
296 $tagline = $this->postText(
'brand_tagline', 180);
297 $userDesign = $this->postKey(
'default_design_user', 63);
298 $adminDesign = $this->postKey(
'default_design_admin', 63);
299 $language = strtolower($this->postKey(
'default_lng', 3));
300 $timezone = $this->postText(
'timezone', 80);
301 $designs = $this->designOptions();
303 if ($siteTitle ===
'') {
304 $this->errors[] =
'Bitte geben Sie einen Seitentitel ein.';
306 if ($brandName ===
'') {
307 $this->errors[] =
'Bitte geben Sie einen Namen für das Branding ein.';
309 if (!isset($designs[$userDesign])) {
310 $this->errors[] =
'Das gewählte Standarddesign ist nicht verfügbar.';
312 if (!isset($designs[$adminDesign])) {
313 $this->errors[] =
'Das gewählte Admin-Design ist nicht verfügbar.';
315 if (!in_array($language, array(
'de',
'en',
'es'),
true)) {
316 $this->errors[] =
'Die Standardsprache ist ungültig.';
318 if (!in_array($timezone, timezone_identifiers_list(),
true)) {
319 $this->errors[] =
'Die Zeitzone ist ungültig.';
321 if ($this->errors !== array()) {
325 $this->setState(
'basics', array(
326 'site_title' => $siteTitle,
327 'brand_name' => $brandName,
328 'brand_tagline' => $tagline,
329 'default_design_user' => $userDesign,
330 'default_design_admin' => $adminDesign,
331 'default_lng' => $language,
332 'timezone' => $timezone,
334 $this->notices[] =
'Grunddaten und Designauswahl wurden übernommen.';
338 private function processDatabase(): bool
340 $mode = $this->postKey(
'storage_mode', 20);
341 if (!in_array(
$mode, array(
'sqlite',
'mysql',
'configured'),
true)) {
342 $this->errors[] =
'Bitte wählen Sie einen gültigen Datenspeicher.';
345 if (
$mode !==
'sqlite' && !$this->postBool(
'storage_advanced_confirm')) {
346 $this->errors[] =
'Bitte bestätigen Sie die erweiterte Datenbankauswahl. Die mitgelieferten DB3-Datenbanken sind der einfache Standard und können auch später noch auf einen PDO-Server umgestellt werden.';
350 if (
$mode ===
'sqlite') {
351 if (!extension_loaded(
'pdo_sqlite')) {
352 $this->errors[] =
'Für SQLite/DB3 muss die Erweiterung pdo_sqlite aktiviert sein.';
355 $bindings =
dbx()->get_config(
'dbx',
'dd_server_bindings', array());
356 if (is_array($bindings) && $bindings !== array()) {
358 $this->errors[] =
'Die lokalen DD-Serverbindungen konnten nicht zurückgesetzt werden.';
362 $this->setState(
'database', array(
367 'checked_at' => time(),
369 $this->notices[] =
'Tabellenstruktur und Fachdaten der ausgelieferten DB3-Dateien bleiben unverändert. Den persönlichen Administratorzugang legen Sie in Schritt 5 fest.';
373 if (
$mode ===
'configured') {
374 $this->setState(
'database', array(
375 'mode' =>
'configured',
379 'checked_at' => time(),
381 $this->notices[] =
'Die bereits eingerichteten Datenbankziele der Module werden verwendet.';
385 $dbType = strtolower($this->postKey(
'db_type', 20));
387 'mysql' =>
'pdo_mysql',
388 'pgsql' =>
'pdo_pgsql',
389 'sqlsrv' =>
'pdo_sqlsrv',
391 if (!isset($pdoTypes[$dbType])) {
392 $this->errors[] =
'Bitte wählen Sie einen unterstützten PDO-Datenbanktyp.';
395 if (!extension_loaded($pdoTypes[$dbType])) {
396 $this->errors[] =
'Für den gewählten PDO-Server muss die Erweiterung '
397 . $pdoTypes[$dbType] .
' aktiviert sein.';
401 $host = $this->postText(
'db_host', 255);
402 $database = $this->postDatabaseName(
'db_name');
403 $user = $this->postText(
'db_user', 255);
404 $password = $this->postSecret(
'db_password', 1024);
406 $current =
dbx()->get_config(
'dbx',
'db', array());
408 ? (string)($current[self::SQL_SERVER][
'pass'] ??
'')
411 $port = (int)$this->postInt(
'db_port', 3306);
412 $create = $this->postBool(
'db_create');
413 $migrate = $this->postBool(
'migrate_data');
414 if ($host ===
'' || $database ===
'' ||
$user ===
'') {
415 $this->errors[] =
'PDO-Server, Datenbank und Benutzer sind erforderlich.';
417 if ($port < 1 || $port > 65535) {
418 $this->errors[] =
'Der Datenbankport muss zwischen 1 und 65535 liegen.';
420 if ($this->errors !== array()) {
428 'dbname' => $database,
431 'port' => (
string)$port,
433 $db =
dbx()->get_system_obj(
'dbxDB');
434 if (!is_object(
$db)) {
435 $this->errors[] =
'Der Datenbankdienst steht nicht zur Verfügung.';
438 if (!
$db->can_connect_database_config($dbConfig,
true)) {
440 || !
$db->ensure_database_exists(self::SQL_SERVER, $dbConfig)
441 || !
$db->can_connect_database_config($dbConfig,
true)
443 $this->errors[] =
'Die PDO-Verbindung zur Zieldatenbank ist fehlgeschlagen. Die Datenbank muss vorhanden oder erfolgreich angelegt worden sein.';
449 'db' => array(self::SQL_SERVER => $dbConfig),
451 $this->errors[] =
'Die lokale SQL-Konfiguration konnte nicht gespeichert werden.';
456 $this->installer()->bindAllToServer(self::SQL_SERVER);
457 }
catch (Throwable $exception) {
458 $this->errors[] = $exception->getMessage();
462 $this->setState(
'database', array(
464 'server' => self::SQL_SERVER,
467 'database' => $database,
470 'migrate_data' => $migrate,
472 'checked_at' => time(),
474 $this->notices[] =
'PDO-Verbindung, Zieldatenbank und DD-Serverbindungen wurden erfolgreich vorbereitet.';
478 private function processSchema(): bool
482 $database = $this->state(
'database', array());
483 if (($database[
'mode'] ??
'') ===
'sqlite') {
486 throw new RuntimeException(implode(
491 $this->setState(
'schema', array(
496 'groups_created' => 0,
500 .
' ausgelieferte DB3-Tabellen wurden lesend geprüft; es wurde nichts verändert.';
506 throw new RuntimeException(implode(
'; ', (array)(
$schema[
'errors'] ?? array())));
516 if (($database[
'mode'] ??
'') ===
'mysql'
517 && !empty($database[
'migrate_data'])
520 (
string)($database[
'server'] ?? self::SQL_SERVER)
523 throw new RuntimeException(implode(
'; ', (array)(
$transfer[
'errors'] ?? array())));
528 $this->setState(
'schema', array(
529 'total' => (
int)(
$schema[
'total'] ?? 0),
530 'finished' => (
int)(
$schema[
'finished'] ?? 0),
531 'transferred' => (
int)(
$transfer[
'transferred'] ?? 0),
532 'skipped' => (
int)(
$transfer[
'skipped'] ?? 0),
533 'groups_created' => count((array)(
$groups[
'created'] ?? array())),
535 $this->notices[] = (int)(
$schema[
'finished'] ?? 0)
536 .
' DD-Strukturen wurden erfolgreich synchronisiert.';
538 }
catch (Throwable $exception) {
539 $this->errors[] =
'Synchronisierung fehlgeschlagen: ' . $exception->getMessage();
544 private function processAdmin(): bool
546 $passwordMinLength = $this->postInt(
'password_min_length', 6);
547 if ($passwordMinLength < 6 || $passwordMinLength > 128) {
548 $this->errors[] =
'Die Passwort-Mindestlänge muss zwischen 6 und 128 Zeichen liegen. Empfohlen sind mindestens 12 Zeichen.';
551 $password = $this->postSecret(
'admin_password', 128);
552 $passwordRepeat = $this->postSecret(
'admin_password_repeat', 128);
553 if (!hash_equals(
$password, $passwordRepeat)) {
554 $this->errors[] =
'Die beiden Admin-Passwörter stimmen nicht überein.';
556 $missingCriteria = $this->passwordCriteriaMissing(
560 if ($missingCriteria !== array()) {
561 $this->errors[] =
'Das Admin-Passwort erfüllt noch nicht: '
562 . implode(
', ', $missingCriteria) .
'.';
564 if ($this->errors !== array()) {
569 $basics = $this->state(
'basics', array());
570 $admin = $this->installer()->ensureInitialAdmin(
572 (
string)($basics[
'default_lng'] ??
'de'),
576 if (empty(
$admin[
'exists'])) {
577 $this->errors[] =
'Der persönliche Administratorzugang konnte nicht bereitgestellt werden.';
581 $reset = !empty(
$admin[
'reset']);
582 $this->setState(
'admin', array(
583 'email' => (
string)(
$admin[
'email'] ??
''),
586 'reset' => $reset ? 1 : 0,
587 'default_password' => !empty(
$admin[
'default_password']) ? 1 : 0,
588 'password_reset_required' => !empty(
$admin[
'password_reset_required']) ? 1 : 0,
589 'password_min_length' => $passwordMinLength,
590 'password_configured' => 1,
593 $this->notices[] =
'Der Benutzer admin wurde mit dem persönlichen Installationspasswort angelegt.';
595 $this->notices[] =
'Das Passwort des vorhandenen Benutzers admin wurde durch das persönliche Installationspasswort ersetzt.';
598 }
catch (Throwable $exception) {
599 $this->errors[] = $exception->getMessage();
604 private function processMail(): bool
606 $mode = $this->postKey(
'mail_delivery_mode', 20);
607 if (!in_array(
$mode, array(
'internal',
'disabled',
'external'),
true)) {
608 $this->errors[] =
'Die globale E-Mail-Betriebsart ist ungültig.';
612 $transport = $this->postKey(
'mail_transport', 20);
613 $host = $this->postText(
'mail_host', 255);
614 $port = (int)$this->postInt(
'mail_port', 587);
615 $secure = $this->postKey(
'mail_secure', 10);
616 $auth = $this->postBool(
'mail_auth');
617 $user = $this->postText(
'mail_user', 255);
618 $password = $this->postSecret(
'mail_password', 2048);
620 $current =
dbx()->get_config(
'dbx',
'mail', array());
622 ? (string)($current[self::SQL_SERVER][
'pass'] ??
'')
625 $fromEmail = strtolower($this->postText(
'mail_from_email', 254));
626 $fromName = $this->postText(
'mail_from_name', 160);
627 $sender = strtolower($this->postText(
'mail_sender', 254));
628 $domains = strtolower($this->postText(
'mail_from_domains', 1000));
629 $forceFrom = $this->postBool(
'mail_force_from');
630 $sendTest = $this->postBool(
'mail_send_test');
631 $testRecipient = strtolower($this->postText(
'mail_test_recipient', 254));
632 $contactFrom = strtolower($this->postText(
'contact_mail_from', 254));
633 $shopFrom = strtolower($this->postText(
'shop_mail_from', 254));
634 if ($contactFrom ===
'') {
635 $contactFrom = $this->moduleSenderAddress(
'kontakt', $fromEmail);
637 if ($shopFrom ===
'') {
638 $shopFrom = $this->moduleSenderAddress(
'shop', $fromEmail);
641 if (
$mode ===
'external') {
642 if (!in_array($transport, array(
'mail',
'smtp',
'sendmail'),
true)) {
643 $this->errors[] =
'Der Mailtransport ist ungültig.';
645 if (!in_array($secure, array(
'',
'tls',
'ssl'),
true)) {
646 $this->errors[] =
'Die SMTP-Verschlüsselung ist ungültig.';
648 if ($port < 1 || $port > 65535) {
649 $this->errors[] =
'Der SMTP-Port muss zwischen 1 und 65535 liegen.';
651 if (filter_var($fromEmail, FILTER_VALIDATE_EMAIL) ===
false) {
652 $this->errors[] =
'Für externen Versand ist eine gültige Absenderadresse erforderlich.';
654 if ($fromName ===
'') {
655 $this->errors[] =
'Für externen Versand ist ein Absendername erforderlich.';
657 if ($sender !==
'' && filter_var($sender, FILTER_VALIDATE_EMAIL) ===
false) {
658 $this->errors[] =
'Der technische Envelope-Absender ist ungültig.';
660 if ($transport ===
'smtp' && $host ===
'') {
661 $this->errors[] =
'Für SMTP ist ein Servername erforderlich.';
663 if ($transport ===
'smtp' && $auth && (
$user ===
'' ||
$password ===
'')) {
665 $this->errors[] =
'Für SMTP-Authentifizierung werden Benutzer und Passwort benötigt.';
669 && filter_var($testRecipient, FILTER_VALIDATE_EMAIL) ===
false
671 $this->errors[] =
'Für die Test-E-Mail ist eine gültige Empfängeradresse erforderlich.';
677 $transport = in_array($transport, array(
'mail',
'smtp',
'sendmail'),
true)
680 $secure = in_array($secure, array(
'',
'tls',
'ssl'),
true)
683 $port = ($port >= 1 && $port <= 65535) ? $port : 587;
685 if (filter_var($contactFrom, FILTER_VALIDATE_EMAIL) ===
false) {
686 $this->errors[] =
'Der Absender für Kontaktanfragen ist ungültig.';
688 if (filter_var($shopFrom, FILTER_VALIDATE_EMAIL) ===
false) {
689 $this->errors[] =
'Der Absender für Shop-Nachrichten ist ungültig.';
691 if ($this->errors !== array()) {
696 'transport' => $transport,
698 'port' => (
string)$port,
700 'auth' => $auth ?
'1' :
'0',
703 'from_email' => $fromEmail,
704 'from_name' => $fromName,
705 'from_domains' => $domains,
707 'force_from' => $forceFrom ?
'1' :
'0',
710 'mail_delivery_mode' =>
$mode,
711 'default_mail' => self::SQL_SERVER,
712 'mail' => array(self::SQL_SERVER =>
$profile),
714 $this->errors[] =
'Die lokale E-Mail-Konfiguration konnte nicht gespeichert werden.';
718 'mail_profile' => self::SQL_SERVER,
719 'mail_from' => $contactFrom,
721 $this->errors[] =
'Der Absender für Kontaktanfragen konnte nicht lokal gespeichert werden.';
725 'mail_profile' => self::SQL_SERVER,
726 'mail_from' => $shopFrom,
728 $this->errors[] =
'Der Absender für Shop-Nachrichten konnte nicht lokal gespeichert werden.';
732 if (
$mode ===
'external' && $sendTest) {
734 array(
'email' => $fromEmail,
'name' => $fromName),
736 'dbxapp Installation: E-Mail-Test',
737 '<p>Der E-Mail-Versand dieser dbxapp-Installation funktioniert.</p>',
741 'text' =>
'Der E-Mail-Versand dieser dbxapp-Installation funktioniert.',
742 'mail_profile' => self::SQL_SERVER,
746 $mail =
dbx()->get_system_obj(
'dbxMail');
747 $reason = is_object(
$mail) ? trim((
string)
$mail->get_error()) :
'';
748 $this->errors[] =
'Die Test-E-Mail konnte nicht gesendet werden'
749 . ($reason !==
'' ?
': ' . $reason :
'.');
752 $this->notices[] =
'Die Test-E-Mail wurde erfolgreich versendet.';
755 $this->setState(
'mail', array(
757 'transport' => $transport,
761 'auth' => $auth ? 1 : 0,
763 'from_email' => $fromEmail,
764 'from_name' => $fromName,
766 'from_domains' => $domains,
767 'force_from' => $forceFrom ? 1 : 0,
768 'contact_from' => $contactFrom,
769 'shop_from' => $shopFrom,
770 'test_recipient' => $testRecipient,
771 'tested' => (
$mode ===
'external' && $sendTest) ? 1 : 0,
773 'checked_at' => time(),
775 if (
$mode ===
'internal') {
776 $this->notices[] =
'E-Mail-Ereignisse bleiben intern; es wird nichts nach außen versendet.';
777 } elseif (
$mode ===
'disabled') {
778 $this->notices[] =
'E-Mail-Ereignisse werden global abgelehnt.';
780 $this->notices[] =
'Der externe E-Mail-Versand ist konfiguriert.';
785 private function processFinish(): bool
787 if (!$this->postBool(
'confirm_install')) {
788 $this->errors[] =
'Bitte bestätigen Sie die Zusammenfassung vor dem Abschluss.';
791 if ((
int)$this->state(
'completed', 0) < 6) {
792 $this->errors[] =
'Die notwendigen Installationsschritte sind noch nicht vollständig.';
796 $basics = $this->state(
'basics', array());
797 $admin = (array)$this->state(
'admin', array());
800 $secret = trim((
string)(
$config[
'secure'] ??
''));
801 if (strlen($secret) < 32) {
802 $secret = bin2hex(random_bytes(32));
810 'site_title' => (
string)($basics[
'site_title'] ??
'dbxapp'),
811 'page' => (
string)($basics[
'site_title'] ??
'dbxapp'),
812 'brand_name' => (
string)($basics[
'brand_name'] ??
'dbxapp'),
813 'brand_tagline' => (
string)($basics[
'brand_tagline'] ??
''),
814 'default_design_user' => (
string)($basics[
'default_design_user'] ??
'dbxapp'),
815 'default_design_admin' => (
string)($basics[
'default_design_admin'] ??
'dbxapp'),
816 'default_lng' => (
string)($basics[
'default_lng'] ??
'de'),
817 'timezone' => (
string)($basics[
'timezone'] ??
'Europe/Berlin'),
818 'password_min_length' => max(
820 min(128, (
int)(
$admin[
'password_min_length'] ?? 6))
824 $this->errors[] =
'Der Installationsabschluss konnte nicht in config.local.php gespeichert werden.';
828 dbx()->set_system_var(
'dbx_install', 0);
829 $this->finishResult = array(
830 'site_title' => $patch[
'site_title'],
831 'admin_email' => (
string)($this->state(
'admin', array())[
'email'] ??
''),
832 'mail_mode' => (
string)($this->state(
'mail', array())[
'mode'] ??
'internal'),
833 'database' => (array)$this->state(
'database', array()),
835 $this->finished =
true;
836 $this->setState(
'completed', self::TOTAL_STEPS);
837 dbx()->invalidate_action_tokens();
841 private function renderStep(
int $step): string
843 return match ($step) {
844 1 => $this->renderSystem(),
845 2 => $this->renderBasics(),
846 3 => $this->renderDatabase(),
847 4 => $this->renderSchema(),
848 5 => $this->renderAdmin(),
849 6 => $this->renderMail(),
850 7 => $this->renderFinish(),
855 private function renderSystem(): string
864 $icon = match (
$check[
'status']) {
865 'ok' =>
'bi-check-circle-fill',
866 'error' =>
'bi-x-circle-fill',
867 default =>
'bi-exclamation-triangle-fill',
869 $rows .=
'<div class="dbx-install-check dbx-install-check--' . $this->h(
$check[
'status']) .
'">'
870 .
'<span class="dbx-install-check__icon"><i class="bi ' . $icon .
'"></i></span>'
871 .
'<span class="dbx-install-check__body"><strong>' . $this->h(
$check[
'label']) .
'</strong>'
872 .
'<small>' . $this->h(
$check[
'hint']) .
'</small></span>'
873 .
'<span class="dbx-install-check__value">' . $this->h(
$check[
'value']) .
'</span>'
877 $button = $requiredOk
878 ?
'<button class="btn btn-primary btn-lg" type="submit"><span>Weiter zur Grundkonfiguration</span><i class="bi bi-arrow-right"></i></button>'
879 :
'<button class="btn btn-secondary btn-lg" type="button" disabled><i class="bi bi-lock"></i><span>Notwendige Prüfungen beheben</span></button>';
881 return '<div class="dbx-install-section-head"><span class="dbx-install-kicker">Schritt 1</span>'
882 .
'<h2>Ist der Server bereit?</h2>'
883 .
'<p>dbxapp prüft zuerst die wirklich notwendigen Werte. Optionale Erweiterungen können später ergänzt werden.</p></div>'
884 .
'<div class="dbx-install-metrics">'
885 . $this->metric(
'PHP', PHP_VERSION,
'bi-filetype-php')
886 . $this->metric(
'Server', $this->serverLabel(),
'bi-hdd-rack')
887 . $this->metric(
'Speicher', (
string)ini_get(
'memory_limit'),
'bi-memory')
889 .
'<div class="dbx-install-checks">' .
$rows .
'</div>'
891 .
'<div class="dbx-install-actions">'
892 .
'<button class="btn btn-outline-secondary btn-lg" type="button" data-dbx-leave-allow onclick="window.location.reload()"><i class="bi bi-arrow-clockwise"></i><span>Erneut prüfen</span></button>'
897 private function renderBasics(): string
901 $values = array_replace(
903 'site_title' => (
string)(
$config[
'site_title'] ?? (
$config[
'page'] ??
'dbxapp')),
904 'brand_name' => (
string)(
$config[
'brand_name'] ??
'dbxapp'),
905 'brand_tagline' => (
string)(
$config[
'brand_tagline'] ??
''),
906 'default_design_user' => (
string)(
$config[
'default_design_user'] ??
'dbxapp'),
907 'default_design_admin' => (
string)(
$config[
'default_design_admin'] ??
'dbxapp'),
908 'default_lng' => (
string)(
$config[
'default_lng'] ??
'de'),
909 'timezone' => (
string)(
$config[
'timezone'] ??
'Europe/Berlin'),
911 (array)$this->state(
'basics', array())
913 $designOptions = $this->selectOptions(
914 $this->designOptions(),
915 (
string)$values[
'default_design_user']
917 $adminDesignOptions = $this->selectOptions(
918 $this->designOptions(),
919 (
string)$values[
'default_design_admin']
922 return '<div class="dbx-install-section-head"><span class="dbx-install-kicker">Schritt 2</span>'
923 .
'<h2>Identität und Standarddesign</h2>'
924 .
'<p>Diese Angaben bilden den globalen Rahmen. Inhalte und Design können später im Adminbereich weiterentwickelt werden.</p></div>'
926 .
'<div class="row g-4">'
927 .
'<div class="col-lg-7"><div class="dbx-install-card"><h3><i class="bi bi-window"></i> Website</h3>'
928 . $this->input(
'site_title',
'Seitentitel / App-Name', (
string)$values[
'site_title'],
'text',
true,
'Zum Beispiel: Kundenportal Muster GmbH')
929 . $this->input(
'brand_name',
'Markenname', (
string)$values[
'brand_name'],
'text',
true,
'Wird in Branding, Dokumenttitel und E-Mails verwendet.')
930 . $this->input(
'brand_tagline',
'Claim / Kurzbeschreibung', (
string)$values[
'brand_tagline'],
'text',
false,
'Optional, maximal 180 Zeichen')
931 .
'<div class="row g-3"><div class="col-md-6">' . $this->fieldLabel(
'default_lng',
'Standardsprache')
932 .
'<select class="form-select" id="default_lng" name="default_lng">'
933 . $this->selectOptions(array(
'de' =>
'Deutsch',
'en' =>
'English',
'es' =>
'Español'), (
string)$values[
'default_lng'])
934 .
'</select></div><div class="col-md-6">'
935 . $this->input(
'timezone',
'Zeitzone', (
string)$values[
'timezone'],
'text',
true,
'Zum Beispiel: Europe/Berlin')
936 .
'</div></div></div></div>'
937 .
'<div class="col-lg-5"><div class="dbx-install-card"><h3><i class="bi bi-palette2"></i> Design</h3>'
938 . $this->fieldLabel(
'default_design_user',
'Standarddesign')
939 .
'<select class="form-select mb-3" id="default_design_user" name="default_design_user">' . $designOptions .
'</select>'
940 . $this->fieldLabel(
'default_design_admin',
'Admin-Design')
941 .
'<select class="form-select" id="default_design_admin" name="default_design_admin">' . $adminDesignOptions .
'</select>'
942 .
'<p class="dbx-install-help"><i class="bi bi-stars"></i> Das gewählte Design ist der Startpunkt. Es kann später im Design-Studio auch KI-gestützt nach Ihren eigenen Vorgaben angepasst oder als neues Design aufgebaut werden.</p>'
943 .
'<div class="dbx-install-help-actions dbx-install-help-actions--card">'
944 . $this->installerHelpButton(
'design',
'KI-gestützte Designanpassung erklärt',
'bi-magic')
946 .
'</div></div></div>'
947 . $this->actions(2,
'Weiter zum Datenspeicher')
951 private function renderDatabase(): string
956 ?
$config[
'db'][self::SQL_SERVER]
958 $saved = (array)$this->state(
'database', array());
959 $mode = (string)($saved[
'mode'] ??
'sqlite');
960 $dbType = (string)($saved[
'type'] ?? (
$profile[
'type'] ??
'mysql'));
961 return '<div class="dbx-install-section-head"><span class="dbx-install-kicker">Schritt 3</span>'
962 .
'<h2>Wo sollen die Daten liegen?</h2>'
963 .
'<p>Die mitgelieferten DB3-Dateien sind sofort einsatzbereit. Tabellenstruktur und Fachdaten bleiben erhalten; das persönliche Passwort für den Benutzer <strong>admin</strong> legen Sie in Schritt 5 fest. Ein externer PDO-Server ist eine optionale bewusste Umstellung.</p></div>'
965 .
'<div class="dbx-install-choice-grid">'
966 . $this->choice(
'storage_mode',
'sqlite',
$mode,
'bi-device-ssd',
'Mitgelieferte DB3',
'Ohne Datenbankserver starten; den persönlichen Admin-Zugang legen Sie später fest.',
'Standard')
967 . $this->choice(
'storage_mode',
'mysql',
$mode,
'bi-database-check',
'PDO-Datenbankserver',
'MySQL/MariaDB, PostgreSQL oder SQL Server als gemeinsames DD-Ziel.',
'Optional')
968 . $this->choice(
'storage_mode',
'configured',
$mode,
'bi-diagram-3',
'Bereits eingerichtete Datenbanken',
'Vorhandene Datenbankziele der Module unverändert weiterverwenden.',
'Fortgeschritten')
970 .
'<div class="dbx-install-help-actions" aria-label="Hilfe zum Datenspeicher">'
971 . $this->installerHelpButton(
'db3',
'Mitgelieferte DB3 genau erklärt',
'bi-info-circle')
972 . $this->installerHelpButton(
'pdo',
'PDO-Migration Schritt für Schritt',
'bi-arrow-left-right')
974 .
'<div class="alert alert-warning dbx-install-inline-note dbx-install-storage-confirm" data-install-storage-confirm'
975 . (
$mode ===
'sqlite' ?
' hidden' :
'')
976 .
'><i class="bi bi-exclamation-triangle-fill"></i><div><strong>Erweiterte Datenbankauswahl bestätigen</strong>'
977 .
'<span>Die mitgelieferten DB3-Datenbanken sind der einfache, sofort einsatzbereite Standard: Es werden keine Server-Zugangsdaten und keine Migration benötigt. Einen PDO-Server oder bereits eingerichtete Datenbanken können Sie auch später in der Administration auswählen.</span>'
978 . $this->switchInput(
979 'storage_advanced_confirm',
980 'Ich möchte die ausgewählte erweiterte Datenbanklösung jetzt verwenden.',
981 $this->postBool(
'storage_advanced_confirm'),
982 'Die Auswahl wird erst nach dieser ausdrücklichen Bestätigung geprüft und übernommen.'
985 .
'<div class="dbx-install-card dbx-install-db-fields" data-install-db-fields>'
986 .
'<h3><i class="bi bi-server"></i> PDO-Verbindung</h3>'
987 .
'<div class="row g-3">'
988 .
'<div class="col-12">' . $this->fieldLabel(
'db_type',
'Datenbanktyp')
989 .
'<select class="form-select" id="db_type" name="db_type">'
990 . $this->selectOptions(
992 'mysql' =>
'MySQL / MariaDB',
993 'pgsql' =>
'PostgreSQL',
994 'sqlsrv' =>
'Microsoft SQL Server',
999 .
'<div class="col-md-8">' . $this->input(
'db_host',
'Server / Host', (
string)($saved[
'host'] ?? (
$profile[
'host'] ??
'127.0.0.1')),
'text',
false,
'127.0.0.1 oder Hostname') .
'</div>'
1000 .
'<div class="col-md-4">' . $this->input(
'db_port',
'Port', (
string)($saved[
'port'] ?? (
$profile[
'port'] ?? 3306)),
'number',
false,
'3306') .
'</div>'
1001 .
'<div class="col-md-6">' . $this->input(
'db_name',
'Datenbank', (
string)($saved[
'database'] ?? (
$profile[
'dbname'] ??
'dbxapp')),
'text',
false,
'dbxapp') .
'</div>'
1002 .
'<div class="col-md-6">' . $this->input(
'db_user',
'Benutzer', (
string)($saved[
'user'] ?? (
$profile[
'user'] ??
'')),
'text',
false,
'') .
'</div>'
1003 .
'<div class="col-12">' . $this->input(
'db_password',
'Passwort',
'',
'password',
false, !empty(
$profile[
'pass']) ?
'Leer lassen: vorhandenes Passwort beibehalten' :
'Wird nur in config.local.php gespeichert',
'new-password') .
'</div>'
1005 . $this->switchInput(
'db_create',
'Datenbank bei Bedarf automatisch erstellen',
true,
'Benötigt CREATE DATABASE-Recht.')
1006 . $this->switchInput(
'migrate_data',
'Vorhandene lokale DD-Daten auf SQL übertragen', !empty($saved[
'migrate_data']),
'Explizite Migration: vorhandene Zieltabellen werden mit den lokalen Quelldaten ersetzt.')
1008 .
'<div class="alert alert-info dbx-install-inline-note" data-install-storage-note="sqlite"'
1009 . (
$mode ===
'sqlite' ?
'' :
' hidden')
1010 .
'><i class="bi bi-shield-check"></i><div><strong>DB3-Standard</strong><span>Für die mitgelieferten DB3-Datenbanken sind keine Server-Zugangsdaten erforderlich. Tabellen und Fachdaten werden nicht neu angelegt oder kopiert. Im Administrationsschritt legen Sie direkt Ihr persönliches Admin-Passwort fest.</span></div></div>'
1011 .
'<div class="alert alert-info dbx-install-inline-note" data-install-storage-note="mysql"'
1012 . (
$mode ===
'mysql' ?
'' :
' hidden')
1013 .
'><i class="bi bi-database-check"></i><div><strong>PDO-Server</strong><span>Der Installer prüft zuerst die Verbindung zur angegebenen Zieldatenbank. Nur nach erfolgreicher Verbindung wird der Server vorbereitet. Tabellen und Daten werden erst im nächsten Schritt verarbeitet.</span></div></div>'
1014 .
'<div class="alert alert-info dbx-install-inline-note" data-install-storage-note="configured"'
1015 . (
$mode ===
'configured' ?
'' :
' hidden')
1016 .
'><i class="bi bi-diagram-3"></i><div><strong>Bereits eingerichtete Datenbanken</strong><span>dbxapp verwendet die schon konfigurierten Datenbankziele der einzelnen Module unverändert weiter. Diese Auswahl ist für bestehende oder besondere Installationen gedacht.</span></div></div>'
1017 . $this->checkedStatus(
1019 'Datenspeicher geprüft',
1021 'mysql' =>
'Die PDO-Verbindung zur Zieldatenbank war erfolgreich. Konfiguration und DD-Serverbindungen wurden übernommen.',
1022 'configured' =>
'Die bereits eingerichteten Datenbankziele der Module wurden übernommen.',
1023 default =>
'PDO SQLite ist verfügbar und der mitgelieferte DB3-Standard wurde ausgewählt. Die Tabellen werden im nächsten Schritt lesend geprüft.',
1026 . $this->actionsWithCheck(
1028 'Weiter zu Strukturen & Daten',
1030 !empty($saved[
'checked'])
1031 ?
'Datenbank erneut prüfen'
1032 :
'Datenbank prüfen'
1037 private function renderSchema(): string
1039 $database = (array)$this->state(
'database', array());
1040 $catalog = $this->installer()->discoverDDs();
1041 $modeLabels = array(
1042 'sqlite' =>
'SQLite / DB3 nach DD-Standard',
1043 'mysql' =>
'Externer PDO-Datenbankserver',
1044 'configured' =>
'Bereits eingerichtete Datenbanken',
1046 $mode = (string)($database[
'mode'] ??
'sqlite');
1047 $migrate = !empty($database[
'migrate_data']);
1048 $bundled =
$mode ===
'sqlite';
1050 return '<div class="dbx-install-section-head"><span class="dbx-install-kicker">Schritt 4</span>'
1052 ?
'<h2>Mitgelieferte DB3-Strukturen bestätigen</h2><p>Beim Weitergehen werden die vorhandenen Datenbanken und Tabellen einmal lesend auf Vollständigkeit geprüft. Danach folgt Schritt 5: Dort legen Sie das persönliche Passwort für den Benutzer <strong>admin</strong> fest.</p>'
1053 :
'<h2>DD-Strukturen synchronisieren</h2><p>dbxapp erzeugt die Datenbanken und Tabellen direkt aus den aktuellen Data Definitions. Nach erfolgreicher Synchronisierung folgt die Einrichtung des Adminzugangs.</p>')
1055 .
'<div class="dbx-install-plan">'
1056 . $this->planRow(
'bi-database',
'Zielspeicher', (
string)($modeLabels[
$mode] ??
$mode))
1060 count(
$catalog) . ($bundled ?
' ausgelieferte Tabellen werden geprüft' :
' Strukturen gefunden')
1065 $bundled ?
'Mitgelieferte Datensätze bleiben unverändert' :
'Gast-, Mitglieder- und Admin-Gruppe'
1068 'bi-arrow-left-right',
1071 ?
'Vorhandene lokale Tabellen werden ausdrücklich übertragen'
1072 : ($bundled ?
'Nicht erforderlich' :
'Keine Übertragung vorhandener Daten')
1076 ?
'<div class="alert alert-warning dbx-install-inline-note"><i class="bi bi-exclamation-triangle"></i><div><strong>Bestätigte Migration</strong><span>Vorhandene SQL-Zieltabellen werden vor der Übertragung geleert. Die lokalen Quelldaten bleiben erhalten.</span></div></div>'
1078 . $this->formOpen(4)
1079 . $this->actions(4,
'Weiter zur Administration')
1083 private function installerHelpButton(
1089 'design' =>
'Design später KI-gestützt weiterentwickeln',
1090 'db3' =>
'Mitgelieferte DB3 verwenden',
1091 'pdo' =>
'Auf einen PDO-Server migrieren',
1092 'email' =>
'E-Mail-Konfiguration verstehen',
1094 $title = (string)(
$titles[$topic] ??
'Hilfe zur Installation');
1095 $width = $topic ===
'email' ?
'900' :
'780';
1096 $url =
'?dbx_modul=dbxSetup&dbx_run1=install&install_help=' . $topic;
1097 $config =
'lib=openWin|id=install-help-' . $topic
1099 .
'|title=' . $title
1100 .
'|mode=modal|width=' . $width .
'|height=82%|position=center'
1101 .
'|closable=1|minimizable=0|maximizable=1|scroll=1|ajax=1';
1102 return '<button type="button" class="btn btn-outline-primary" data-dbx="'
1103 . $this->h(
$config) .
'"><i class="bi ' . $this->h($icon)
1104 .
'"></i><span>' . $this->h($label) .
'</span></button>';
1107 private function renderInstallerHelp(
string $topic): string
1109 if ($topic ===
'design') {
1110 return '<article class="dbx-install-help-window">'
1111 .
'<span class="dbx-install-kicker">Flexibler Designstart</span>'
1112 .
'<h2>Das Design später mit KI weiterentwickeln</h2>'
1113 .
'<p>Die Auswahl während der Installation legt nur das anfängliche Standard- und Admin-Design fest. Sie ist keine endgültige Entscheidung: Das Erscheinungsbild kann später im Design-Studio an Ihre Marke, Zielgruppe und Arbeitsabläufe angepasst oder als eigenes Design neu aufgebaut werden.</p>'
1114 .
'<div class="dbx-install-help-window__facts">'
1115 .
'<section><i class="bi bi-palette2"></i><h3>Bestehendes Design anpassen</h3><p>Farben, Typografie, Abstände, Komponenten, Navigation und Branding lassen sich auf Basis des gewählten Designs gezielt verändern.</p></section>'
1116 .
'<section><i class="bi bi-stars"></i><h3>KI als Gestaltungspartner</h3><p>Beschreiben Sie Stil, Markenwerte, Zielgruppe und gewünschte Funktionen in natürlicher Sprache. Daraus können passende Designvarianten und konkrete Anpassungen entwickelt werden.</p></section>'
1118 .
'<ol class="dbx-install-help-steps">'
1119 .
'<li><strong>Jetzt eine solide Grundlage wählen.</strong><span>Für den Einstieg ist „dbxapp – Standard“ eine sichere und vollständig unterstützte Ausgangsbasis.</span></li>'
1120 .
'<li><strong>Eigene Vorgaben sammeln.</strong><span>Logo, Hausfarben, Schriftwünsche, Beispiele und Anforderungen helfen der KI, ein konsistentes Ergebnis zu erstellen.</span></li>'
1121 .
'<li><strong>Design anpassen oder neu erstellen.</strong><span>Ein vorhandenes Design kann schrittweise verfeinert oder als getrenntes eigenes Design aufgebaut werden.</span></li>'
1122 .
'<li><strong>Vor dem Einsatz prüfen.</strong><span>Desktop, Mobilansicht, Kontraste, Bedienbarkeit und wichtige Formulare sollten anschließend getestet werden.</span></li>'
1124 .
'<div class="alert alert-info"><i class="bi bi-info-circle-fill"></i> Standard- und Admin-Design können unabhängig gewählt und später jederzeit geändert werden. Ihre Inhalte und Daten bleiben davon unberührt.</div>'
1128 if ($topic ===
'db3') {
1129 return '<article class="dbx-install-help-window">'
1130 .
'<span class="dbx-install-kicker">Empfohlener Standard</span>'
1131 .
'<h2>Mitgelieferte DB3 direkt verwenden</h2>'
1132 .
'<p>Die Auslieferung enthält die benötigten <code>*.db3</code>-Dateien bereits mit Tabellen und Grunddaten. Es sind weder Serverdaten noch eine Migration erforderlich.</p>'
1133 .
'<div class="dbx-install-help-window__facts">'
1134 .
'<section><i class="bi bi-database-check"></i><h3>Was der Installer prüft</h3><p>Jede aktuelle Data Definition muss ihre ausgelieferte Tabelle erreichen. Diese Prüfung ist ausschließlich lesend.</p></section>'
1135 .
'<section><i class="bi bi-shield-lock"></i><h3>Was unverändert bleibt</h3><p>Tabellenstruktur und Fachdaten werden weder erzeugt, synchronisiert, geleert noch kopiert. Nur der Benutzer admin wird im Administrationsschritt auf den verbindlichen Installationszugang zurückgesetzt.</p></section>'
1137 .
'<ol class="dbx-install-help-steps">'
1138 .
'<li><strong>DB3-Standard auswählen.</strong><span>Die Auswahl ist bereits voreingestellt.</span></li>'
1139 .
'<li><strong>Zur Administration weitergehen.</strong><span>Beim Wechsel prüft der Installer die DB3-Dateien einmal lesend. Fehlende Dateien oder Tabellen stoppen den Ablauf mit einer eindeutigen Meldung.</span></li>'
1140 .
'<li><strong>Administratorzugang bereitstellen.</strong><span>Im Administrationsschritt wird der Benutzer admin angelegt oder sein bestehendes Passwort durch Ihr persönliches Passwort ersetzt.</span></li>'
1141 .
'<li><strong>Direkt sicher anmelden.</strong><span>Nach der Installation verwenden Sie admin und das bereits während der Installation festgelegte persönliche Passwort.</span></li>'
1143 .
'<div class="alert alert-info"><i class="bi bi-info-circle-fill"></i> DB3 eignet sich für den direkten Start, lokale Installationen und portable Sicherungen.</div>'
1147 if ($topic ===
'pdo') {
1148 return '<article class="dbx-install-help-window">'
1149 .
'<span class="dbx-install-kicker">Optionale Migration</span>'
1150 .
'<h2>DB3-Daten auf einen PDO-Server übertragen</h2>'
1151 .
'<p>Die PDO-Option ist für MySQL/MariaDB, PostgreSQL oder Microsoft SQL Server vorgesehen. Verbindung, Datenbank und Migration werden bewusst getrennt geprüft.</p>'
1152 .
'<ol class="dbx-install-help-steps">'
1153 .
'<li><strong>Serverdaten erfassen.</strong><span>Datenbanktyp, Host, Port, Datenbankname und Benutzer sind erforderlich. Vorhandene Werte werden vorbelegt; ein leeres Passwortfeld behält das gespeicherte Passwort.</span></li>'
1154 .
'<li><strong>Zieldatenbank verbinden.</strong><span>Ist sie vorhanden, muss eine direkte PDO-Verbindung gelingen. Optional darf dbxapp eine fehlende Datenbank anlegen.</span></li>'
1155 .
'<li><strong>Verbindung erneut prüfen.</strong><span>Erst eine erfolgreiche Verbindung zur vorhandenen oder neu angelegten Zieldatenbank schaltet den nächsten Schritt frei.</span></li>'
1156 .
'<li><strong>DD-Strukturen erzeugen.</strong><span>Die Tabellen werden aus den aktuellen Data Definitions auf dem verbundenen Ziel aufgebaut.</span></li>'
1157 .
'<li><strong>Daten nur auf Wunsch übertragen.</strong><span>Die zusätzliche Option kopiert die vorhandenen DB3-Daten. Zieltabellen werden dafür vor dem Kopieren geleert; die DB3-Quellen bleiben erhalten.</span></li>'
1159 .
'<div class="alert alert-warning"><i class="bi bi-exclamation-triangle-fill"></i> Ohne erfolgreiche Zielverbindung startet weder eine Schemaänderung noch eine Datenübertragung.</div>'
1163 return '<article class="dbx-install-help-window">'
1164 .
'<span class="dbx-install-kicker">Globaler Maildienst</span>'
1165 .
'<h2>E-Mail-Versand sicher konfigurieren</h2>'
1166 .
'<p>Der globale Maildienst entscheidet, ob dbxapp Nachrichten nur intern protokolliert, vollständig ablehnt oder über einen echten Mailserver versendet. Module können unterschiedliche sichtbare Absender verwenden, solange der ausgewählte Mailserver diese Adressen akzeptiert.</p>'
1167 .
'<div class="dbx-install-help-window__facts">'
1168 .
'<section><i class="bi bi-toggle2-on"></i><h3>Betriebsart</h3><p><strong>Nur intern</strong> sendet nichts ins Netzwerk. <strong>Extern senden</strong> aktiviert den Transport. <strong>Vollständig aus</strong> lehnt Mailereignisse ab.</p></section>'
1169 .
'<section><i class="bi bi-hdd-network"></i><h3>Transport</h3><p>SMTP ist für produktive Systeme meist die beste Wahl. PHP mail() und Sendmail setzen eine korrekt eingerichtete Serverumgebung voraus.</p></section>'
1170 .
'<section><i class="bi bi-person-badge"></i><h3>Sichtbarer Absender</h3><p>Absenderadresse und Absendername erscheinen im From-Header. Ohne Moduladresse dienen sie als globaler Standard.</p></section>'
1171 .
'<section><i class="bi bi-arrow-return-left"></i><h3>Envelope-Absender</h3><p>Der technische Envelope-Absender verarbeitet Rückläufer und wird häufig als Return-Path verwendet. Er ist nicht die normale Antwortadresse.</p></section>'
1173 .
'<ol class="dbx-install-help-steps">'
1174 .
'<li><strong>Betriebsart festlegen.</strong><span>Für die sichere Erstinstallation ist „Nur intern“ empfohlen. Externer Versand kann später aktiviert werden.</span></li>'
1175 .
'<li><strong>Mailserver eintragen.</strong><span>Bei SMTP werden Host, Port, Verschlüsselung und gegebenenfalls Benutzer und Passwort benötigt. Ein leeres Passwortfeld behält ein bereits gespeichertes Passwort.</span></li>'
1176 .
'<li><strong>Globalen Standardabsender angeben.</strong><span>Verwenden Sie eine reale Adresse Ihrer eigenen Domain, die beim Mailanbieter als Absender zugelassen ist.</span></li>'
1177 .
'<li><strong>Geschäftsbereiche trennen.</strong><span>Kontaktanfragen und Shop-Nachrichten erhalten eigene frei wählbare From-Adressen. Diese Werte werden lokal als <code>dbxContact.mail_from</code> und <code>dbxShop.mail_from</code> gespeichert und können von nachgelagerten E-Mail-Prozessen eindeutig gefiltert werden.</span></li>'
1178 .
'<li><strong>Absenderdomains zuordnen.</strong><span>Die Domainliste hilft dbxMail bei mehreren Mailprofilen, den passenden Transport zu finden. Sie ersetzt keine Freigabe beim Provider und keine SPF-, DKIM- oder DMARC-Konfiguration.</span></li>'
1179 .
'<li><strong>Test-E-Mail senden.</strong><span>Der Test ist nur bei externem Versand möglich und geht an die beim Administrator hinterlegte Adresse.</span></li>'
1181 .
'<h3 class="dbx-install-help-subtitle">Empfohlene Absenderrollen</h3>'
1182 .
'<div class="dbx-install-help-sender-grid">'
1183 .
'<section><i class="bi bi-shield-lock"></i><h3>System & Administration</h3><p>Zum Beispiel <code>system@ihre-domain.de</code> für Registrierung, Sicherheit und technische Benachrichtigungen.</p></section>'
1184 .
'<section><i class="bi bi-chat-left-text"></i><h3>Kontakt</h3><p>Standardmäßig <code>kontakt@ihre-domain.de</code>. Benachrichtigung, Eingangsbestätigung und Supportantwort verwenden diesen Absender; die Adresse des Anfragenden bleibt Reply-To.</p></section>'
1185 .
'<section><i class="bi bi-bag-check"></i><h3>Shop</h3><p>Standardmäßig <code>shop@ihre-domain.de</code> für Bestellungen, Statusmeldungen und Widerrufe.</p></section>'
1187 .
'<div class="alert alert-info"><i class="bi bi-info-circle-fill"></i> Ist „Globalen Absender für alle Module erzwingen“ aktiviert, ersetzt der globale From-Absender jede Moduladresse. Das ist hilfreich, wenn der Mailanbieter nur eine einzige Absenderadresse erlaubt.</div>'
1188 .
'<div class="alert alert-warning"><i class="bi bi-exclamation-triangle-fill"></i> Zugangsdaten werden ausschließlich lokal gespeichert. Veröffentlichen Sie SMTP-Passwörter niemals im Repository.</div>'
1192 private function renderAdmin(): string
1197 $admin = $this->installer()->inspectInitialAdmin();
1198 }
catch (Throwable $exception) {
1199 $admin = array(
'exists' =>
false,
'default_password' =>
false);
1201 $adminExists = !empty(
$admin[
'exists']);
1202 $savedAdmin = (array)$this->state(
'admin', array());
1203 $passwordMinLength = (int)($savedAdmin[
'password_min_length']
1204 ?? (
$config[
'password_min_length'] ?? 6));
1205 if ($this->isPost() && isset(
$_POST[
'password_min_length'])) {
1206 $passwordMinLength = $this->postInt(
'password_min_length', 6);
1208 return '<div class="dbx-install-section-head"><span class="dbx-install-kicker">Schritt 5</span>'
1209 .
'<h2>Administrator absichern</h2>'
1210 .
'<p>Der Benutzername lautet immer <strong>admin</strong>. Legen Sie jetzt direkt das persönliche Passwort fest, mit dem Sie sich nach der Installation anmelden.</p></div>'
1211 . $this->formOpen(5,
'off')
1212 .
'<div class="row g-4"><div class="col-lg-7"><div class="dbx-install-card">'
1213 .
'<h3><i class="bi bi-person-lock"></i> Persönlicher Administratorzugang</h3>'
1214 . $this->input(
'initial_admin_user',
'Benutzername',
'admin',
'text',
false,
'',
'username',
true)
1215 .
'<div class="row g-3"><div class="col-md-6">'
1216 . $this->input(
'admin_password',
'Admin-Passwort',
'',
'password',
true,
'Persönliches Passwort',
'new-password')
1217 .
'</div><div class="col-md-6">'
1218 . $this->input(
'admin_password_repeat',
'Passwort wiederholen',
'',
'password',
true,
'Noch einmal eingeben',
'new-password')
1220 .
'<div class="mb-3">' . $this->fieldLabel(
'password_min_length',
'Mindestlänge für neue Passwörter',
true)
1221 .
'<input class="form-control" id="password_min_length" name="password_min_length" type="number" min="6" max="128" step="1" required value="' . $this->h((
string)$passwordMinLength) .
'">'
1222 .
'<p class="dbx-install-field-note"><i class="bi bi-info-circle"></i> Standard: 6 Zeichen. Für höhere Sicherheit werden 12 oder mehr Zeichen empfohlen. Dieselbe Vorgabe gilt später bei erzwungenen Passwortänderungen.</p></div>'
1224 ?
'<div class="alert alert-warning mb-0"><i class="bi bi-exclamation-triangle-fill"></i> Das bisherige Passwort des vorhandenen Benutzers <strong>admin</strong> wird durch das hier eingegebene persönliche Passwort ersetzt.</div>'
1225 :
'<div class="alert alert-info mb-0"><i class="bi bi-info-circle-fill"></i> Das persönliche Passwort wird direkt sicher gehasht gespeichert und nicht in die Installationszusammenfassung übernommen.</div>')
1226 .
'</div></div><div class="col-lg-5"><div class="dbx-install-card dbx-install-security-card">'
1227 .
'<i class="bi bi-shield-check"></i><h3>Passwortanforderungen</h3>'
1228 .
'<ul class="dbx-install-password-rules" data-install-password-rules data-min-length="' . $this->h((
string)$passwordMinLength) .
'">'
1229 .
'<li data-password-rule="length"><i class="bi bi-circle"></i><span>mindestens <strong data-password-min-label>' . $this->h((
string)$passwordMinLength) .
'</strong> Zeichen; 12 oder mehr empfohlen</span></li>'
1230 .
'<li data-password-rule="letters"><i class="bi bi-circle"></i><span>Groß- und Kleinbuchstaben</span></li>'
1231 .
'<li data-password-rule="number"><i class="bi bi-circle"></i><span>mindestens eine Zahl</span></li>'
1232 .
'<li data-password-rule="special"><i class="bi bi-circle"></i><span>mindestens ein Sonderzeichen</span></li>'
1233 .
'<li data-password-rule="match"><i class="bi bi-circle"></i><span>beide Eingaben stimmen überein</span></li>'
1234 .
'</ul><p>Die Kriterien werden beim Tippen sofort geprüft.</p>'
1235 .
'</div></div></div>'
1236 . $this->actions(5,
'Administratorzugang speichern')
1240 private function renderMail(): string
1245 ?
$config[
'mail'][self::SQL_SERVER]
1247 $admin = (array)$this->state(
'admin', array());
1248 $basics = (array)$this->state(
'basics', array());
1249 $saved = (array)$this->state(
'mail', array());
1250 $mode = (string)($saved[
'mode'] ?? (
$config[
'mail_delivery_mode'] ??
'internal'));
1251 $transport = (string)($saved[
'transport'] ?? (
$profile[
'transport'] ??
'smtp'));
1252 $globalFrom = (string)($saved[
'from_email']
1258 $contactDerived = $this->moduleSenderAddress(
'kontakt', $globalFrom);
1259 $contactFrom = (string)($saved[
'contact_from']
1261 if (filter_var($contactFrom, FILTER_VALIDATE_EMAIL) ===
false
1262 || (!array_key_exists(
'contact_from', $saved)
1263 && strtolower($contactFrom) ===
'kontakt@dbxapp.de'
1264 && $contactDerived !==
'kontakt@dbxapp.de')
1266 $contactFrom = $contactDerived;
1268 $shopDerived = $this->moduleSenderAddress(
'shop', $globalFrom);
1269 $shopFrom = (string)($saved[
'shop_from']
1271 if (filter_var($shopFrom, FILTER_VALIDATE_EMAIL) ===
false
1272 || (!array_key_exists(
'shop_from', $saved)
1273 && strtolower($shopFrom) ===
'shop@dbxapp.de'
1274 && $shopDerived !==
'shop@dbxapp.de')
1276 $shopFrom = $shopDerived;
1278 $testRecipient = (string)($saved[
'test_recipient']
1279 ?? (
$admin[
'email'] ??
''));
1280 if (filter_var($testRecipient, FILTER_VALIDATE_EMAIL) ===
false
1281 && filter_var($globalFrom, FILTER_VALIDATE_EMAIL) !==
false
1283 $testRecipient = $globalFrom;
1286 return '<div class="dbx-install-section-head"><span class="dbx-install-kicker">Schritt 6</span>'
1287 .
'<h2>Globaler E-Mail-Betrieb</h2>'
1288 .
'<p>Der Schalter gilt zentral für alle Module. Standardmäßig bleiben E-Mail-Ereignisse intern und verlassen den Server nicht.</p></div>'
1289 .
'<div class="dbx-install-help-actions" aria-label="Hilfe zur E-Mail-Konfiguration">'
1290 . $this->installerHelpButton(
'email',
'E-Mail-Konfiguration ausführlich erklärt',
'bi-question-circle')
1292 . $this->formOpen(6,
'off')
1293 .
'<div class="dbx-install-choice-grid dbx-install-choice-grid--compact">'
1294 . $this->choice(
'mail_delivery_mode',
'internal',
$mode,
'bi-inbox',
'Nur intern',
'Kein externer Versand; Ereignisse bleiben im System.',
'Sicherer Standard')
1295 . $this->choice(
'mail_delivery_mode',
'external',
$mode,
'bi-send-check',
'Extern senden',
'SMTP, PHP-Mail oder Sendmail aktivieren.',
'Aktiv')
1296 . $this->choice(
'mail_delivery_mode',
'disabled',
$mode,
'bi-envelope-slash',
'Vollständig aus',
'Alle Mailereignisse global ablehnen.',
'Gesperrt')
1298 .
'<div class="dbx-install-card dbx-install-mail-card" data-install-mail-fields'
1299 . (
$mode ===
'external' ?
'' :
' hidden')
1300 .
'><div class="dbx-install-mail-sections">'
1301 .
'<section class="dbx-install-mail-section"><h3><i class="bi bi-hdd-network"></i> Versandserver</h3>'
1302 .
'<div class="row g-2"><div class="col-md-4">' . $this->fieldLabel(
'mail_transport',
'Transport')
1303 .
'<select class="form-select" id="mail_transport" name="mail_transport">'
1304 . $this->selectOptions(array(
'smtp' =>
'SMTP',
'mail' =>
'PHP mail()',
'sendmail' =>
'Sendmail'), $transport)
1305 .
'</select></div><div class="col-md-5">'
1306 . $this->input(
'mail_host',
'SMTP-Server', (
string)($saved[
'host'] ?? (
$profile[
'host'] ??
'')),
'text',
false,
'smtp.example.org')
1307 .
'</div><div class="col-md-3">'
1308 . $this->input(
'mail_port',
'Port', (
string)($saved[
'port'] ?? (
$profile[
'port'] ?? 587)),
'number',
false,
'587')
1309 .
'</div><div class="col-md-4">' . $this->fieldLabel(
'mail_secure',
'Verschlüsselung')
1310 .
'<select class="form-select" id="mail_secure" name="mail_secure">'
1311 . $this->selectOptions(array(
'tls' =>
'TLS / STARTTLS',
'ssl' =>
'SSL / SMTPS',
'' =>
'Keine'), (
string)($saved[
'secure'] ?? (
$profile[
'secure'] ??
'tls')))
1312 .
'</select></div><div class="col-md-8 d-flex align-items-end">'
1313 . $this->switchInput(
'mail_auth',
'SMTP-Authentifizierung', !empty($saved[
'auth'] ??
$profile[
'auth']),
'')
1314 .
'</div><div class="col-md-6">'
1315 . $this->input(
'mail_user',
'SMTP-Benutzer', (
string)($saved[
'user'] ?? (
$profile[
'user'] ??
'')),
'text',
false,
'meist die E-Mail-Adresse',
'username')
1316 .
'</div><div class="col-md-6">'
1324 ?
'Leer lassen: vorhandenes Passwort beibehalten'
1325 :
'Wird lokal und nicht im Release gespeichert',
1328 .
'</div></div></section>'
1329 .
'<section class="dbx-install-mail-section"><h3><i class="bi bi-person-vcard"></i> Globaler Absender</h3>'
1330 .
'<div class="row g-2"><div class="col-md-6">'
1331 . $this->input(
'mail_from_email',
'Absenderadresse', $globalFrom,
'email',
false,
'noreply@example.org')
1332 .
'</div><div class="col-md-6">'
1333 . $this->input(
'mail_from_name',
'Absendername', (
string)($saved[
'from_name'] ?? (
$profile[
'from_name'] ?? ($basics[
'brand_name'] ??
'dbxapp'))),
'text',
false,
'Name der Website')
1334 .
'</div><div class="col-md-6">'
1335 . $this->input(
'mail_sender',
'Envelope-Absender', (
string)($saved[
'sender'] ?? (
$profile[
'sender'] ??
'')),
'email',
false,
'Optional, für Bounces')
1336 .
'</div><div class="col-md-6">'
1337 . $this->input(
'mail_from_domains',
'Erlaubte Domains', (
string)($saved[
'from_domains'] ?? (
$profile[
'from_domains'] ??
'')),
'text',
false,
'example.org,*.example.org')
1338 .
'</div></div></section>'
1339 .
'<section class="dbx-install-mail-section"><h3><i class="bi bi-signpost-split"></i> Modul-Absender</h3>'
1340 .
'<p class="dbx-install-mail-intro">Getrennte Adressen erleichtern die automatische Zuordnung beim Empfänger.</p>'
1341 .
'<div class="row g-2"><div class="col-md-6">'
1342 . $this->input(
'contact_mail_from',
'Kontaktanfragen', $contactFrom,
'email',
true,
'kontakt@example.org')
1343 .
'<p class="dbx-install-field-note">Anfragende bleiben als Reply-To erhalten.</p>'
1344 .
'</div><div class="col-md-6">'
1345 . $this->input(
'shop_mail_from',
'Shop-Nachrichten', $shopFrom,
'email',
true,
'shop@example.org')
1346 .
'<p class="dbx-install-field-note">Für Bestellungen, Status und Widerrufe.</p>'
1348 . $this->switchInput(
'mail_force_from',
'Globalen Absender erzwingen', !empty($saved[
'force_from'] ??
$profile[
'force_from']),
'Nur wenn der Mailanbieter keine unterschiedlichen Absender erlaubt.')
1350 .
'<section class="dbx-install-mail-section"><h3><i class="bi bi-envelope-check"></i> Versandtest</h3>'
1351 . $this->input(
'mail_test_recipient',
'Empfänger der Test-E-Mail', $testRecipient,
'email',
false,
'admin@example.org')
1352 . $this->switchInput(
'mail_send_test',
'Beim Prüfen eine Test-E-Mail senden',
false,
'Nur bei „Extern senden“. Ohne diese ausdrückliche Auswahl wird keine E-Mail versendet.')
1353 .
'</section></div></div>'
1354 . $this->checkedStatus(
1356 'E-Mail-Konfiguration geprüft',
1357 !empty($saved[
'tested'])
1358 ?
'Die Konfiguration ist gültig und die Test-E-Mail wurde erfolgreich versendet.'
1359 :
'Die Angaben sind gültig und lokal gespeichert. Es wurde keine Test-E-Mail versendet.'
1361 . $this->actionsWithCheck(
1363 'Weiter zur Abschlussprüfung',
1365 !empty($saved[
'checked'])
1366 ?
'E-Mail erneut prüfen'
1370 .
'<script>document.addEventListener("DOMContentLoaded",function(){const box=document.querySelector("[data-install-mail-fields]");const radios=document.querySelectorAll("input[name=mail_delivery_mode]");function sync(){const active=document.querySelector("input[name=mail_delivery_mode]:checked");if(box)box.hidden=!active||active.value!=="external";}radios.forEach(function(r){r.addEventListener("change",sync)});sync();const globalFrom=document.getElementById("mail_from_email");const roles=[["contact_mail_from","kontakt"],["shop_mail_from","shop"]];function domain(){const value=(globalFrom&&globalFrom.value||"").trim().toLowerCase();const at=value.lastIndexOf("@");return at>0&&at<value.length-1?value.slice(at+1):"";}roles.forEach(function(entry){const input=document.getElementById(entry[0]);if(!input||!globalFrom)return;const initialDomain=domain();const initialAuto=entry[1]+"@"+initialDomain;let automatic=input.value.trim().toLowerCase()===initialAuto||input.value.trim().toLowerCase()===entry[1]+"@dbxapp.de";input.addEventListener("input",function(){automatic=false;});globalFrom.addEventListener("input",function(){const nextDomain=domain();if(automatic&&nextDomain)input.value=entry[1]+"@"+nextDomain;});});});</script>';
1373 private function renderFinish(): string
1375 $basics = (array)$this->state(
'basics', array());
1376 $database = (array)$this->state(
'database', array());
1377 $schema = (array)$this->state(
'schema', array());
1378 $admin = (array)$this->state(
'admin', array());
1379 $mail = (array)$this->state(
'mail', array());
1380 $databaseLabel = match ((
string)($database[
'mode'] ??
'sqlite')) {
1381 'mysql' => strtoupper((
string)($database[
'type'] ??
'mysql'))
1382 .
' · ' . (string)($database[
'host'] ??
'')
1383 .
' / ' . (string)($database[
'database'] ??
''),
1384 'configured' =>
'Bereits eingerichtete Datenbanken',
1385 default =>
'SQLite / DB3 nach DD-Standard',
1387 $mailLabel = match ((
string)(
$mail[
'mode'] ??
'internal')) {
1388 'external' =>
'Externer Versand über ' . strtoupper((
string)(
$mail[
'transport'] ??
'smtp')),
1389 'disabled' =>
'Vollständig deaktiviert',
1390 default =>
'Nur interne Annahme, kein Netzwerkversand',
1392 $contactFrom = (string)(
$mail[
'contact_from']
1393 ?? $this->moduleSenderAddress(
'kontakt', (
string)(
$mail[
'from_email'] ??
'')));
1394 $shopFrom = (string)(
$mail[
'shop_from']
1395 ?? $this->moduleSenderAddress(
'shop', (
string)(
$mail[
'from_email'] ??
'')));
1397 return '<div class="dbx-install-section-head"><span class="dbx-install-kicker">Schritt 7</span>'
1398 .
'<h2>Bereit für den Start</h2>'
1399 .
'<p>Geheimnisse werden in dieser Übersicht bewusst nicht angezeigt.</p></div>'
1400 .
'<div class="dbx-install-summary">'
1401 . $this->summaryRow(
'bi-window',
'Website', (
string)($basics[
'site_title'] ??
''), (
string)($basics[
'brand_name'] ??
''))
1402 . $this->summaryRow(
'bi-palette',
'Design', (
string)($basics[
'default_design_user'] ??
'dbxapp'),
'Admin: ' . (
string)($basics[
'default_design_admin'] ??
'dbxapp'))
1403 . $this->summaryRow(
'bi-database-check',
'Daten', $databaseLabel, (
int)(
$schema[
'finished'] ?? 0) .
' DD-Strukturen synchronisiert')
1404 . $this->summaryRow(
'bi-person-check',
'Administrator',
'admin',
'Persönliches Passwort eingerichtet · Mindestlänge ' . (
string)(
$admin[
'password_min_length'] ?? 6) .
' Zeichen')
1405 . $this->summaryRow(
'bi-envelope-check',
'E-Mail', $mailLabel, (
string)(
$mail[
'from_email'] ??
''))
1406 . $this->summaryRow(
'bi-chat-left-text',
'Kontakt-Absender', $contactFrom,
'dbxContact.mail_from')
1407 . $this->summaryRow(
'bi-bag-check',
'Shop-Absender', $shopFrom,
'dbxShop.mail_from')
1409 . $this->formOpen(7)
1410 .
'<label class="dbx-install-confirm"><input class="form-check-input" type="checkbox" name="confirm_install" value="1" required>'
1411 .
'<span><strong>Installation verbindlich abschließen' . $this->tooltipIcon($this->fieldTooltip(
'confirm_install')) .
'</strong><small>dbxapp setzt den lokalen Installationsschalter auf 0 und startet danach im normalen Betriebsmodus.</small></span></label>'
1412 .
'<div class="dbx-install-actions">' . $this->backLink(7)
1413 .
'<button class="btn btn-success btn-lg" type="submit"><i class="bi bi-check2-circle"></i><span>dbxapp jetzt aktivieren</span></button>'
1417 private function renderShell(
int $step,
string $content): string
1420 foreach ($this->errors as $message) {
1421 $alerts .=
'<div class="alert alert-danger dbx-install-alert"><i class="bi bi-x-octagon-fill"></i><span>' . $this->h($message) .
'</span></div>';
1423 foreach ($this->notices as $message) {
1424 $alerts .=
'<div class="alert alert-success dbx-install-alert"><i class="bi bi-check-circle-fill"></i><span>' . $this->h($message) .
'</span></div>';
1427 $completed = (int)$this->state(
'completed', 0);
1429 foreach ($this->steps as $number =>
$definition) {
1430 $class = $number === $step
1432 : ($number <= $completed ?
'is-complete' :
'is-pending');
1433 $inner =
'<span class="dbx-install-step__number">'
1434 . ($number <= $completed && $number !== $step
1435 ?
'<i class="bi bi-check-lg"></i>'
1437 .
'</span><span class="dbx-install-step__text"><small>'
1439 .
'</small><strong>' . $this->h(
$definition[
'title']) .
'</strong></span>';
1440 if ($number <= $completed + 1 && $number !== $step) {
1441 $nav .=
'<a class="dbx-install-step ' .
$class .
'" href="' . $this->h($this->stepUrl($number)) .
'">' . $inner .
'</a>';
1443 $nav .=
'<span class="dbx-install-step ' .
$class .
'">' . $inner .
'</span>';
1446 $progress = (int)round((($step - 1) / self::TOTAL_STEPS) * 100);
1448 return '<div class="dbx-install-layout">'
1449 .
'<aside class="dbx-install-sidebar">'
1450 .
'<a class="dbx-install-brand" href="' . $this->h($this->stepUrl(1)) .
'"><span class="dbx-install-brand__mark"><i class="bi bi-boxes"></i></span>'
1451 .
'<span><strong>dbxapp</strong><small>Installationsassistent</small></span></a>'
1452 .
'<div class="dbx-install-progress"><span style="width:' . $progress .
'%"></span></div>'
1453 .
'<nav aria-label="Installationsschritte">' . $nav .
'</nav>'
1454 .
'<div class="dbx-install-sidebar__foot"><i class="bi bi-shield-lock"></i><span>Lokale Konfiguration<br><strong>Release-geschützt</strong></span></div>'
1456 .
'<main class="dbx-install-main"><header class="dbx-install-topbar">'
1457 .
'<div><span>Erstinstallation</span><strong>Schritt ' . $step .
' von ' . self::TOTAL_STEPS .
'</strong></div>'
1458 .
'<span class="dbx-install-version">Entwicklungsstand ' . $this->h($this->version()) .
'</span>'
1459 .
'</header><section class="dbx-install-content">'
1461 .
'<div class="dbx-install-surface">' .
$content .
'</div>'
1462 .
'</section></main></div>';
1465 private function renderFinished(): string
1467 $database = (array)($this->finishResult[
'database'] ?? array());
1468 $databaseLabel = ($database[
'mode'] ??
'') ===
'mysql'
1469 ?
'PDO / ' . strtoupper((
string)($database[
'type'] ??
'mysql'))
1470 : (($database[
'mode'] ??
'') ===
'configured' ?
'Bereits eingerichtete Datenbanken' :
'SQLite / DB3');
1471 return '<div class="dbx-install-finished"><div class="dbx-install-finished__glow"></div>'
1472 .
'<div class="dbx-install-finished__card"><span class="dbx-install-finished__icon"><i class="bi bi-check2"></i></span>'
1473 .
'<span class="dbx-install-kicker">Installation abgeschlossen</span>'
1474 .
'<h1>' . $this->h((
string)($this->finishResult[
'site_title'] ??
'dbxapp')) .
' ist startklar.</h1>'
1475 .
'<p>Systemprüfung, DD-Strukturen, Administrator und globale E-Mail-Regeln sind eingerichtet. Melden Sie sich jetzt als <strong>admin</strong> mit dem während der Installation festgelegten persönlichen Passwort an.</p>'
1476 .
'<div class="dbx-install-finished__facts">'
1477 .
'<span><i class="bi bi-database-check"></i><strong>' . $this->h($databaseLabel) .
'</strong><small>Datenspeicher</small></span>'
1478 .
'<span><i class="bi bi-person-check"></i><strong>admin</strong><small>Persönliches Passwort eingerichtet</small></span>'
1479 .
'<span><i class="bi bi-envelope-check"></i><strong>' . $this->h($this->mailModeLabel((
string)($this->finishResult[
'mail_mode'] ??
'internal'))) .
'</strong><small>E-Mail-Betrieb</small></span>'
1480 .
'</div><a class="btn btn-light btn-lg" href="?dbx_modul=dbxLogin&dbx_run1=login"><span>Zur Anmeldung</span><i class="bi bi-arrow-right"></i></a>'
1481 .
'<small class="dbx-install-finished__hint">Der lokale Installationsschalter wurde sicher auf 0 gesetzt.</small>'
1485 private function renderAlreadyInstalled(): string
1487 return '<div class="dbx-install-finished"><div class="dbx-install-finished__card">'
1488 .
'<span class="dbx-install-finished__icon"><i class="bi bi-shield-check"></i></span>'
1489 .
'<span class="dbx-install-kicker">Systemstatus</span><h1>dbxapp ist bereits installiert.</h1>'
1490 .
'<p>Der Installationsassistent ist im laufenden Betrieb gesperrt.</p>'
1491 .
'<a class="btn btn-light btn-lg" href="' . $this->h(
dbx()->
get_base_url()) .
'"><span>Zur Anwendung</span><i class="bi bi-arrow-right"></i></a>'
1495 private function formOpen(
int $step,
string $autocomplete =
'on'): string
1497 return '<form class="dbx-install-form" method="post" action="' . $this->h($this->stepUrl($step)) .
'" autocomplete="' . $this->h($autocomplete) .
'">'
1498 .
'<input type="hidden" name="install_token" value="' . $this->h(
dbx()->
action_token($this->tokenScope($step))) .
'">';
1501 private function actions(
int $step,
string $nextLabel): string
1503 return '<div class="dbx-install-actions">' . $this->backLink($step)
1504 .
'<button class="btn btn-primary btn-lg" type="submit"><span>' . $this->h($nextLabel) .
'</span><i class="bi bi-arrow-right"></i></button></div>';
1507 private function actionsWithCheck(
1510 string $checkAction,
1513 return
'<div class="dbx-install-actions">' . $this->backLink($step)
1514 .
'<div class="dbx-install-action-group">'
1515 .
'<button class="btn btn-outline-primary btn-lg" type="submit" name="install_action" value="' . $this->h($checkAction) .
'"><i class="bi bi-arrow-clockwise"></i><span>' . $this->h($checkLabel) .
'</span></button>'
1516 .
'<button class="btn btn-primary btn-lg" type="submit" name="install_action" value="continue"><span>' . $this->h($nextLabel) .
'</span><i class="bi bi-arrow-right"></i></button>'
1523 private function checkedStatus(
1529 return '<div class="alert alert-secondary dbx-install-inline-note dbx-install-check-status" data-install-check-status data-install-checked="0">'
1530 .
'<i class="bi bi-clock-history"></i><div><strong>In dieser Installationssitzung noch nicht geprüft</strong>'
1531 .
'<span>Die vorhandenen Vorgaben sind geladen. Mit „Prüfen“ können Sie den aktuellen Stand kontrollieren, ohne zum nächsten Schritt zu wechseln.</span></div></div>';
1534 $checkedAt = (int)(
$state[
'checked_at'] ?? 0);
1535 $when = $checkedAt > 0
1536 ? date(
'd.m.Y, H:i', $checkedAt) .
' Uhr'
1537 :
'in dieser Installationssitzung';
1539 return '<div class="alert alert-success dbx-install-inline-note dbx-install-check-status" data-install-check-status data-install-checked="1">'
1540 .
'<i class="bi bi-check-circle-fill"></i><div><strong>' . $this->h($title) .
'</strong>'
1541 .
'<span>' . $this->h($detail) .
' Zuletzt erfolgreich: ' . $this->h($when) .
'.</span></div></div>';
1544 private function backLink(
int $step): string
1547 return '<span></span>';
1549 return '<a class="btn btn-outline-secondary btn-lg" href="' . $this->h($this->stepUrl($step - 1)) .
'"><i class="bi bi-arrow-left"></i><span>Zurück</span></a>';
1552 private function fieldLabel(
1557 return
'<label class="form-label" for="' . $this->h($name) .
'">'
1559 . (
$required ?
' <span aria-hidden="true">*</span>' :
'')
1560 . $this->tooltipIcon($this->fieldTooltip($name))
1564 private function tooltipIcon(
string $tooltip): string
1566 if ($tooltip ===
'') {
1569 return ' <span class="dbx-install-tooltip" tabindex="0" role="button"'
1570 .
' data-dbx-tooltip="' . $this->h($tooltip) .
'"'
1571 .
' aria-label="Hilfe: ' . $this->h($tooltip) .
'">'
1572 .
'<i class="bi bi-question-circle-fill" aria-hidden="true"></i></span>';
1575 private function fieldTooltip(
string $name): string
1578 'site_title' =>
'Erscheint im Browser-Titel, in Seitentiteln und an zentralen Stellen der Anwendung.',
1579 'brand_name' =>
'Sichtbarer Name der Website oder Organisation, unter anderem für Branding und E-Mails.',
1580 'brand_tagline' =>
'Kurzer Zusatz zum Markennamen, zum Beispiel Leistungsversprechen oder Themen der Website.',
1581 'default_lng' =>
'Sprache, die dbxapp für neue oder noch nicht angemeldete Besucher zuerst verwendet.',
1582 'timezone' =>
'Steuert Datums- und Zeitangaben. Für Deutschland ist Europe/Berlin passend.',
1583 'default_design_user' =>
'Anfängliches Design für öffentliche Seiten und angemeldete Anwender.',
1584 'default_design_admin' =>
'Anfängliches Erscheinungsbild des Administrationsbereichs.',
1585 'storage_advanced_confirm' =>
'Bestätigt bewusst eine erweiterte Datenbanklösung anstelle des einfachen DB3-Standards.',
1586 'db_type' =>
'Typ des externen PDO-Servers. Er muss zur installierten PHP-PDO-Erweiterung passen.',
1587 'db_host' =>
'Hostname oder IP-Adresse des Datenbankservers, zum Beispiel 127.0.0.1 oder db.example.org.',
1588 'db_port' =>
'Netzwerkport des Datenbankservers, zum Beispiel 3306 für MySQL oder MariaDB.',
1589 'db_name' =>
'Name der vorhandenen oder anzulegenden Zieldatenbank.',
1590 'db_user' =>
'Datenbankbenutzer mit den benötigten Rechten für Verbindung, Tabellen und optional die Datenbankanlage.',
1591 'db_password' =>
'Passwort des Datenbankbenutzers. Leer lassen behält ein bereits lokal gespeichertes Passwort bei.',
1592 'db_create' =>
'Legt die Datenbank nur dann an, wenn sie fehlt und der Benutzer das CREATE-DATABASE-Recht besitzt.',
1593 'migrate_data' =>
'Überträgt lokale DB3-Daten ausdrücklich auf den PDO-Server und ersetzt dort vorhandene Zieldaten.',
1594 'initial_admin_user' =>
'Der verbindliche Benutzername des ersten Administrators lautet admin.',
1595 'admin_password' =>
'Persönliches Passwort des Administrators. Es wird ausschließlich als sicherer Passwort-Hash gespeichert.',
1596 'admin_password_repeat' =>
'Wiederholen Sie das persönliche Admin-Passwort, um Eingabefehler auszuschließen.',
1597 'password_min_length' =>
'Kleinster erlaubter Wert für neue Passwörter. Standard sind 6 Zeichen; empfohlen werden mindestens 12.',
1598 'mail_transport' =>
'SMTP ist meist die beste Wahl. PHP mail() und Sendmail benötigen eine passende Serverkonfiguration.',
1599 'mail_host' =>
'Hostname des SMTP-Servers, zum Beispiel smtp.example.org.',
1600 'mail_port' =>
'SMTP-Port des Anbieters, meist 587 für STARTTLS oder 465 für SMTPS.',
1601 'mail_secure' =>
'Verschlüsselung zum SMTP-Server gemäß Vorgabe des Mailanbieters.',
1602 'mail_auth' =>
'Aktiviert die Anmeldung am SMTP-Server mit Benutzername und Passwort.',
1603 'mail_user' =>
'Anmeldename des SMTP-Kontos; häufig die vollständige E-Mail-Adresse.',
1604 'mail_password' =>
'Passwort oder App-Passwort des SMTP-Kontos. Leer lassen behält den vorhandenen lokalen Wert.',
1605 'mail_from_email' =>
'Globale From-Adresse für allgemeine Systemnachrichten.',
1606 'mail_from_name' =>
'Anzeigename, den Empfänger neben der Absenderadresse sehen.',
1607 'mail_sender' =>
'Optionaler technischer Envelope-Absender für Rückläufer und Bounce-Verarbeitung.',
1608 'mail_from_domains' =>
'Kommagetrennte Liste erlaubter Absenderdomains für Modul-Absender.',
1609 'contact_mail_from' =>
'Eigene From-Adresse für Kontaktanfragen; Anfragende bleiben als Reply-To erhalten.',
1610 'shop_mail_from' =>
'Eigene From-Adresse für Bestellungen, Statusmeldungen und Widerrufe.',
1611 'mail_force_from' =>
'Ersetzt Modul-Absender durch die globale Adresse, falls der Anbieter nur einen Absender zulässt.',
1612 'mail_test_recipient' =>
'Adresse, an die der ausdrücklich aktivierte Versandtest geschickt wird.',
1613 'mail_send_test' =>
'Sendet nur bei externem Versand eine echte Test-E-Mail an den angegebenen Empfänger.',
1614 'confirm_install' =>
'Schließt die Installation ab und deaktiviert den Installationsmodus lokal.',
1616 return (
string)($tooltips[$name] ??
'');
1619 private function input(
1623 string $type =
'text',
1625 string $placeholder =
'',
1626 string $autocomplete =
'',
1627 bool $readonly =
false
1629 $input =
'<input class="form-control" id="' . $this->h($name) .
'" name="' . $this->h($name) .
'" type="' . $this->h($type) .
'" value="' . $this->h(
$value) .
'"'
1630 . ($placeholder !==
'' ?
' placeholder="' . $this->h($placeholder) .
'"' :
'')
1631 . ($autocomplete !==
'' ?
' autocomplete="' . $this->h($autocomplete) .
'"' :
'')
1632 . ($readonly ?
' readonly' :
'')
1634 if ($type ===
'password') {
1635 $input =
'<div class="dbx-install-password-control">'
1637 .
'<button type="button" class="dbx-install-password-toggle" data-dbx-password-toggle="' . $this->h($name) .
'" aria-label="Passwort anzeigen" title="Passwort anzeigen" aria-pressed="false">'
1638 .
'<i class="bi bi-eye" aria-hidden="true"></i></button></div>';
1641 return '<div class="mb-3">' . $this->fieldLabel($name, $label,
$required)
1645 private function switchInput(
1651 $tooltip = $this->fieldTooltip($name);
1652 return '<label class="dbx-install-switch"><input class="form-check-input" type="checkbox" name="' . $this->h($name) .
'" value="1"' . (
$checked ?
' checked' :
'') .
'>'
1653 .
'<span><strong>' . $this->h($label) . $this->tooltipIcon($tooltip !==
'' ? $tooltip : $hint) .
'</strong>'
1654 . ($hint !==
'' ?
'<small>' . $this->h($hint) .
'</small>' :
'')
1655 .
'</span></label>';
1658 private function choice(
1664 string $description,
1667 return
'<label class="dbx-install-choice" data-dbx-tooltip="' . $this->h($description) .
'"><input type="radio" name="' . $this->h($name) .
'" value="' . $this->h(
$value) .
'"' . (
$value === $selected ?
' checked' :
'') .
'>'
1668 .
'<span class="dbx-install-choice__body"><span class="dbx-install-choice__top"><i class="bi ' . $this->h($icon) .
'"></i><em>' . $this->h($badge) .
'</em></span>'
1669 .
'<strong>' . $this->h($title) .
'</strong><small>' . $this->h($description) .
'</small><span class="dbx-install-choice__check"><i class="bi bi-check-lg"></i></span></span></label>';
1672 private function metric(
string $label,
string $value,
string $icon): string
1674 return '<div class="dbx-install-metric"><i class="bi ' . $this->h($icon) .
'"></i><span><small>' . $this->h($label) .
'</small><strong>' . $this->h(
$value) .
'</strong></span></div>';
1677 private function planRow(
string $icon,
string $label,
string $value): string
1679 return '<div class="dbx-install-plan__row"><i class="bi ' . $this->h($icon) .
'"></i><span><small>' . $this->h($label) .
'</small><strong>' . $this->h(
$value) .
'</strong></span><i class="bi bi-check-circle-fill"></i></div>';
1682 private function summaryRow(
1688 return
'<div class="dbx-install-summary__row"><i class="bi ' . $this->h($icon) .
'"></i><span><small>' . $this->h($label) .
'</small><strong>' . $this->h(
$value) .
'</strong>'
1689 . ($detail !==
'' ?
'<em>' . $this->h($detail) .
'</em>' :
'')
1690 .
'</span><i class="bi bi-check2"></i></div>';
1696 private function designOptions(): array
1700 foreach (glob(rtrim(
$root,
'/\\') .
'/*', GLOB_ONLYDIR) ?: array() as $directory) {
1701 $name = basename($directory);
1702 if (preg_match(
'/^[A-Za-z0-9][A-Za-z0-9_-]{1,62}$/', $name) !== 1
1703 || !is_file($directory .
'/htm/default.htm')
1707 $title = $name ===
'dbxapp'
1708 ?
'dbxapp – Standard'
1709 : ucfirst(str_replace(array(
'-',
'_'),
' ', $name));
1710 $metadataFile = $directory .
'/design.json';
1711 if (is_file($metadataFile)) {
1712 $metadata = json_decode((
string)file_get_contents($metadataFile),
true);
1714 $title = trim((
string)
$metadata[
'title']);
1720 $options[
'dbxapp'] =
'dbxapp – Standard';
1722 ksort(
$options, SORT_NATURAL | SORT_FLAG_CASE);
1726 private function selectOptions(array
$options,
string $selected): string
1730 $html .=
'<option value="' . $this->h((
string)
$value) .
'"'
1731 . ((string)
$value === $selected ?
' selected' :
'')
1732 .
'>' . $this->h((
string)$label) .
'</option>';
1737 private function installer(): dbxInstallationService
1739 dbx()->get_include_obj(
'dbxInstallationService',
'dbxSetup');
1740 return new dbxInstallationService();
1743 private function requestedStep(): int
1746 $step = filter_var(
$value, FILTER_VALIDATE_INT);
1747 return max(1, min(self::TOTAL_STEPS, $step ===
false ? 1 : (
int)$step));
1750 private function stepUrl(
int $step): string
1752 return '?dbx_modul=dbxSetup&dbx_run1=install&step='
1753 . max(1, min(self::TOTAL_STEPS, $step));
1756 private function isPost(): bool
1758 return strtoupper((
string)(
$_SERVER[
'REQUEST_METHOD'] ??
'GET')) ===
'POST';
1761 private function tokenScope(
int $step): string
1763 return 'dbxSetup.install.step.' . $step;
1766 private function validToken(
int $step): bool
1769 ? (string)
$_POST[
'install_token']
1771 return dbx()->check_action_token($this->tokenScope($step),
$token);
1774 private function state(
string $key, $default =
null)
1776 return dbx()->get_session_var(
1779 self::SESSION_SECTION,
1780 self::SESSION_MODULE
1784 private function setState(
string $key,
$value): void
1786 dbx()->set_session_var(
1789 self::SESSION_SECTION,
1790 self::SESSION_MODULE
1794 private function postText(
string $name,
int $maxLength): string
1797 $value = preg_replace(
'/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u',
'',
$value) ??
'';
1798 return function_exists(
'mb_substr')
1799 ? mb_substr(
$value, 0, $maxLength,
'UTF-8')
1800 : substr(
$value, 0, $maxLength);
1803 private function postKey(
string $name,
int $maxLength): string
1805 $value = $this->postText($name, $maxLength);
1806 return preg_match(
'/^[A-Za-z0-9_.\/:+-]*$/',
$value) === 1 ?
$value :
'';
1809 private function postDatabaseName(
string $name): string
1811 $value = $this->postText($name, 64);
1812 return preg_match(
'/^[A-Za-z0-9_$-]+$/',
$value) === 1 ?
$value :
'';
1815 private function postSecret(
string $name,
int $maxLength): string
1818 return substr(str_replace(
"\0",
'',
$value), 0, $maxLength);
1821 private function postInt(
string $name,
int $default): int
1823 $value = filter_var(
$_POST[$name] ??
null, FILTER_VALIDATE_INT);
1827 private function postBool(
string $name): bool
1829 return isset(
$_POST[$name])
1830 && in_array(strtolower((
string)
$_POST[$name]), array(
'1',
'true',
'on',
'yes'),
true);
1833 private function directoryWritable(
string $directory): bool
1835 if (!is_dir($directory)
1836 && !@mkdir($directory, 0775,
true)
1837 && !is_dir($directory)
1841 if (!is_writable($directory)) {
1844 $file = @tempnam($directory,
'dbx-inst-');
1845 if (
$file ===
false) {
1848 $ok = @file_put_contents(
$file,
'ok', LOCK_EX) !==
false
1849 && @file_get_contents(
$file) ===
'ok';
1854 private function compactPath(
string $path): string
1863 private function iniBytes(
string $value): int
1873 return match (substr(
$value, -1)) {
1874 'g' => (int)($number * 1024 * 1024 * 1024),
1875 'm' => (int)($number * 1024 * 1024),
1876 'k' => (int)($number * 1024),
1877 default => (int)$number,
1881 private function serverLabel(): string
1883 $software = trim((
string)(
$_SERVER[
'SERVER_SOFTWARE'] ??
'PHP'));
1884 if ($software ===
'') {
1887 $parts = preg_split(
'/\s+/', $software);
1888 $label = (string)($parts[0] ?? $software);
1889 return function_exists(
'mb_substr')
1890 ? mb_substr($label, 0, 32,
'UTF-8')
1891 : substr($label, 0, 32);
1894 private function version(): string
1897 return is_file(
$file) ? trim((
string)file_get_contents(
$file)) :
'aktuell';
1900 private function mailModeLabel(
string $mode): string
1902 return match (
$mode) {
1903 'external' =>
'Extern',
1904 'disabled' =>
'Aus',
1905 default =>
'Nur intern',
1909 private function moduleSenderAddress(
string $localPart,
string $globalFrom): string
1911 $localPart = strtolower(trim($localPart));
1912 $domain =
'dbxapp.de';
1913 if (filter_var($globalFrom, FILTER_VALIDATE_EMAIL) !==
false) {
1914 $candidate = strtolower((
string)substr(
1915 strrchr($globalFrom,
'@') ?:
'',
1918 if ($candidate !==
'') {
1919 $domain = $candidate;
1922 return $localPart .
'@' . $domain;
1928 private function passwordCriteriaMissing(
1938 private function h(
string $value): string
1940 return htmlspecialchars(
$value, ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8');