dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxInstall.class.php
Go to the documentation of this file.
1<?php
2
3declare(strict_types=1);
4
5namespace dbx\dbxSetup;
6
7require_once dirname(__DIR__, 3) . '/include/dbxPasswordPolicy.class.php';
8
9use RuntimeException;
10use Throwable;
11
21{
22 private const TOTAL_STEPS = 7;
23 private const SESSION_SECTION = 'installer';
24 private const SESSION_MODULE = 'dbxSetup';
25 private const SQL_SERVER = 'dbxApp';
26
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'),
36 );
37
39 private array $errors = array();
40
42 private array $notices = array();
43
44 private bool $finished = false;
45
46 private bool $stayOnStep = false;
47
49 private array $finishResult = array();
50
51 public function __construct()
52 {
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');
59 }
60 }
61
65 public function run(): string
66 {
67 $config = dbx()->get_config('dbx');
68 $config = is_array($config) ? $config : array();
69 if ((int)($config['install'] ?? 1) !== 1
70 && (int)dbx()->get_system_var('dbx_install', 0, 'int') !== 1
71 ) {
72 return $this->renderAlreadyInstalled();
73 }
74 $help = is_scalar($_GET['install_help'] ?? null)
75 ? strtolower(trim((string)$_GET['install_help']))
76 : '';
77 if (in_array($help, array('design', 'db3', 'pdo', 'email'), true)) {
78 return $this->renderInstallerHelp($help);
79 }
80
81 $completed = max(0, min(
82 self::TOTAL_STEPS,
83 (int)$this->state('completed', 0)
84 ));
85 $step = $this->requestedStep();
86 $step = min($step, min(self::TOTAL_STEPS, $completed + 1));
87
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);
96 }
97 }
98 }
99 }
100
101 if ($this->finished) {
102 return $this->renderFinished();
103 }
104
105 return $this->renderShell($step, $this->renderStep($step));
106 }
107
113 public function systemChecks(): array
114 {
115 $checks = array();
116 $add = static function (
117 string $id,
118 string $label,
119 string $value,
120 bool $ok,
121 bool $required,
122 string $hint = ''
123 ) use (&$checks): void {
124 $checks[] = array(
125 'id' => $id,
126 'label' => $label,
127 'value' => $value,
128 'status' => $ok ? 'ok' : ($required ? 'error' : 'warning'),
129 'required' => $required,
130 'hint' => $hint,
131 );
132 };
133
134 $add(
135 'php',
136 'PHP-Version',
137 PHP_VERSION,
138 version_compare(PHP_VERSION, '8.2.0', '>='),
139 true,
140 'dbxapp benötigt PHP 8.2 oder neuer.'
141 );
142 foreach (array('session', 'PDO', 'json', 'filter', 'hash', 'openssl') as $extension) {
143 $add(
144 'ext-' . strtolower($extension),
145 'PHP-Erweiterung ' . $extension,
146 extension_loaded($extension) ? 'verfügbar' : 'fehlt',
147 extension_loaded($extension),
148 true,
149 'Die Erweiterung muss in php.ini aktiviert sein.'
150 );
151 }
152
153 $configDir = dbx()->os_path(
154 dbx()->get_base_dir() . 'dbx/modules/dbx/cfg'
155 );
156 $localConfigFile = dbx()->os_path(
157 rtrim($configDir, '/\\') . DIRECTORY_SEPARATOR . 'config.local.php'
158 );
159 $localConfigWritable = $this->directoryWritable($configDir)
160 && (!is_file($localConfigFile) || is_writable($localConfigFile));
161 $add(
162 'write-config',
163 'Lokale Konfiguration',
164 $this->compactPath($localConfigFile),
165 $localConfigWritable,
166 true,
167 'Der Webserver benötigt Schreibrecht für config.local.php.'
168 );
169 foreach (array(
170 'files' => dbx()->get_file_dir(),
171 'tmp' => dbx()->get_base_dir() . 'tmp/',
172 ) as $name => $directory) {
173 $directory = dbx()->os_path($directory);
174 $add(
175 'write-' . $name,
176 'Schreibverzeichnis ' . $name . '/',
177 $this->compactPath($directory),
178 $this->directoryWritable($directory),
179 true,
180 'Das Verzeichnis wird für Laufzeitdaten benötigt.'
181 );
182 }
183
184 $autoload = dbx()->os_path(dbx()->get_base_dir() . 'dbx/vendor/autoload.php');
185 $add(
186 'vendor',
187 'Composer-Abhängigkeiten',
188 is_file($autoload) ? 'vollständig' : 'autoload.php fehlt',
189 is_file($autoload) && class_exists('\PHPMailer\PHPMailer\PHPMailer'),
190 true,
191 'Vor der Installation muss composer install ausgeführt worden sein.'
192 );
193
194 foreach (array(
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) {
206 $add(
207 'optional-' . $extension,
208 'Optional: ' . $extension,
209 extension_loaded($extension) ? 'verfügbar' : 'nicht aktiv',
210 extension_loaded($extension),
211 false,
212 'Empfohlen für ' . $purpose . '.'
213 );
214 }
215
216 $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
217 || (int)($_SERVER['SERVER_PORT'] ?? 0) === 443
218 || in_array(
219 strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')),
220 array('https', 'wss'),
221 true
222 );
223 $localHost = in_array(
224 strtolower((string)($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '')),
225 array('localhost', '127.0.0.1', '[::1]'),
226 true
227 );
228 $add(
229 'https',
230 'HTTPS',
231 $https ? 'aktiv' : ($localHost ? 'lokale Entwicklung' : 'nicht erkannt'),
232 $https || $localHost,
233 false,
234 'Produktive Installationen sollten ausschließlich HTTPS verwenden.'
235 );
236
237 $memory = $this->iniBytes((string)ini_get('memory_limit'));
238 $add(
239 'memory',
240 'PHP memory_limit',
241 (string)ini_get('memory_limit'),
242 $memory === -1 || $memory >= 128 * 1024 * 1024,
243 false,
244 'Für Installation und Updates werden mindestens 128 MB empfohlen.'
245 );
246
247 return $checks;
248 }
249
250 private function processStep(int $step): bool
251 {
252 $action = $this->postKey('install_action', 40);
253 $this->stayOnStep = ($step === 3 && $action === 'check_database')
254 || ($step === 6 && $action === 'check_mail');
255
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(),
264 default => false,
265 };
266
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.';
271 }
272
273 return $processed;
274 }
275
276 private function processSystem(): bool
277 {
278 $failed = array_values(array_filter(
279 $this->systemChecks(),
280 static fn(array $check): bool =>
281 $check['required'] && $check['status'] !== 'ok'
282 ));
283 if ($failed !== array()) {
284 $this->errors[] = 'Mindestens eine notwendige Systemvoraussetzung ist noch nicht erfüllt.';
285 return false;
286 }
287
288 $this->notices[] = 'Alle notwendigen Systemvoraussetzungen sind erfüllt.';
289 return true;
290 }
291
292 private function processBasics(): bool
293 {
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();
302
303 if ($siteTitle === '') {
304 $this->errors[] = 'Bitte geben Sie einen Seitentitel ein.';
305 }
306 if ($brandName === '') {
307 $this->errors[] = 'Bitte geben Sie einen Namen für das Branding ein.';
308 }
309 if (!isset($designs[$userDesign])) {
310 $this->errors[] = 'Das gewählte Standarddesign ist nicht verfügbar.';
311 }
312 if (!isset($designs[$adminDesign])) {
313 $this->errors[] = 'Das gewählte Admin-Design ist nicht verfügbar.';
314 }
315 if (!in_array($language, array('de', 'en', 'es'), true)) {
316 $this->errors[] = 'Die Standardsprache ist ungültig.';
317 }
318 if (!in_array($timezone, timezone_identifiers_list(), true)) {
319 $this->errors[] = 'Die Zeitzone ist ungültig.';
320 }
321 if ($this->errors !== array()) {
322 return false;
323 }
324
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,
333 ));
334 $this->notices[] = 'Grunddaten und Designauswahl wurden übernommen.';
335 return true;
336 }
337
338 private function processDatabase(): bool
339 {
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.';
343 return false;
344 }
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.';
347 return false;
348 }
349
350 if ($mode === 'sqlite') {
351 if (!extension_loaded('pdo_sqlite')) {
352 $this->errors[] = 'Für SQLite/DB3 muss die Erweiterung pdo_sqlite aktiviert sein.';
353 return false;
354 }
355 $bindings = dbx()->get_config('dbx', 'dd_server_bindings', array());
356 if (is_array($bindings) && $bindings !== array()) {
357 if (!dbx()->set_local_config_section('dbx', 'dd_server_bindings', array())) {
358 $this->errors[] = 'Die lokalen DD-Serverbindungen konnten nicht zurückgesetzt werden.';
359 return false;
360 }
361 }
362 $this->setState('database', array(
363 'mode' => 'sqlite',
364 'server' => '',
365 'migrate_data' => 0,
366 'checked' => 1,
367 'checked_at' => time(),
368 ));
369 $this->notices[] = 'Tabellenstruktur und Fachdaten der ausgelieferten DB3-Dateien bleiben unverändert. Den persönlichen Administratorzugang legen Sie in Schritt 5 fest.';
370 return true;
371 }
372
373 if ($mode === 'configured') {
374 $this->setState('database', array(
375 'mode' => 'configured',
376 'server' => '',
377 'migrate_data' => 0,
378 'checked' => 1,
379 'checked_at' => time(),
380 ));
381 $this->notices[] = 'Die bereits eingerichteten Datenbankziele der Module werden verwendet.';
382 return true;
383 }
384
385 $dbType = strtolower($this->postKey('db_type', 20));
386 $pdoTypes = array(
387 'mysql' => 'pdo_mysql',
388 'pgsql' => 'pdo_pgsql',
389 'sqlsrv' => 'pdo_sqlsrv',
390 );
391 if (!isset($pdoTypes[$dbType])) {
392 $this->errors[] = 'Bitte wählen Sie einen unterstützten PDO-Datenbanktyp.';
393 return false;
394 }
395 if (!extension_loaded($pdoTypes[$dbType])) {
396 $this->errors[] = 'Für den gewählten PDO-Server muss die Erweiterung '
397 . $pdoTypes[$dbType] . ' aktiviert sein.';
398 return false;
399 }
400
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);
405 if ($password === '') {
406 $current = dbx()->get_config('dbx', 'db', array());
407 $password = is_array($current)
408 ? (string)($current[self::SQL_SERVER]['pass'] ?? '')
409 : '';
410 }
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.';
416 }
417 if ($port < 1 || $port > 65535) {
418 $this->errors[] = 'Der Datenbankport muss zwischen 1 und 65535 liegen.';
419 }
420 if ($this->errors !== array()) {
421 return false;
422 }
423
424 $dbConfig = array(
425 'activ' => '1',
426 'type' => $dbType,
427 'host' => $host,
428 'dbname' => $database,
429 'user' => $user,
430 'pass' => $password,
431 'port' => (string)$port,
432 );
433 $db = dbx()->get_system_obj('dbxDB');
434 if (!is_object($db)) {
435 $this->errors[] = 'Der Datenbankdienst steht nicht zur Verfügung.';
436 return false;
437 }
438 if (!$db->can_connect_database_config($dbConfig, true)) {
439 if (!$create
440 || !$db->ensure_database_exists(self::SQL_SERVER, $dbConfig)
441 || !$db->can_connect_database_config($dbConfig, true)
442 ) {
443 $this->errors[] = 'Die PDO-Verbindung zur Zieldatenbank ist fehlgeschlagen. Die Datenbank muss vorhanden oder erfolgreich angelegt worden sein.';
444 return false;
445 }
446 }
447
448 if (!dbx()->patch_local_config('dbx', array(
449 'db' => array(self::SQL_SERVER => $dbConfig),
450 ))) {
451 $this->errors[] = 'Die lokale SQL-Konfiguration konnte nicht gespeichert werden.';
452 return false;
453 }
454
455 try {
456 $this->installer()->bindAllToServer(self::SQL_SERVER);
457 } catch (Throwable $exception) {
458 $this->errors[] = $exception->getMessage();
459 return false;
460 }
461
462 $this->setState('database', array(
463 'mode' => 'mysql',
464 'server' => self::SQL_SERVER,
465 'type' => $dbType,
466 'host' => $host,
467 'database' => $database,
468 'user' => $user,
469 'port' => $port,
470 'migrate_data' => $migrate,
471 'checked' => 1,
472 'checked_at' => time(),
473 ));
474 $this->notices[] = 'PDO-Verbindung, Zieldatenbank und DD-Serverbindungen wurden erfolgreich vorbereitet.';
475 return true;
476 }
477
478 private function processSchema(): bool
479 {
480 try {
481 $installer = $this->installer();
482 $database = $this->state('database', array());
483 if (($database['mode'] ?? '') === 'sqlite') {
484 $verification = $installer->verifyBundledSchema();
485 if (empty($verification['ok'])) {
486 throw new RuntimeException(implode(
487 '; ',
488 (array)($verification['errors'] ?? array())
489 ));
490 }
491 $this->setState('schema', array(
492 'total' => (int)($verification['total'] ?? 0),
493 'finished' => (int)($verification['verified'] ?? 0),
494 'transferred' => 0,
495 'skipped' => 0,
496 'groups_created' => 0,
497 'read_only' => 1,
498 ));
499 $this->notices[] = (int)($verification['verified'] ?? 0)
500 . ' ausgelieferte DB3-Tabellen wurden lesend geprüft; es wurde nichts verändert.';
501 return true;
502 }
503
504 $schema = $installer->provisionSchema();
505 if (empty($schema['ok'])) {
506 throw new RuntimeException(implode('; ', (array)($schema['errors'] ?? array())));
507 }
508
509 $transfer = array(
510 'ok' => true,
511 'total' => 0,
512 'transferred' => 0,
513 'skipped' => 0,
514 'errors' => array(),
515 );
516 if (($database['mode'] ?? '') === 'mysql'
517 && !empty($database['migrate_data'])
518 ) {
519 $transfer = $installer->transferDeclaredDataToServer(
520 (string)($database['server'] ?? self::SQL_SERVER)
521 );
522 if (empty($transfer['ok'])) {
523 throw new RuntimeException(implode('; ', (array)($transfer['errors'] ?? array())));
524 }
525 }
526
527 $groups = $installer->seedCoreGroups();
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())),
534 ));
535 $this->notices[] = (int)($schema['finished'] ?? 0)
536 . ' DD-Strukturen wurden erfolgreich synchronisiert.';
537 return true;
538 } catch (Throwable $exception) {
539 $this->errors[] = 'Synchronisierung fehlgeschlagen: ' . $exception->getMessage();
540 return false;
541 }
542 }
543
544 private function processAdmin(): bool
545 {
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.';
549 return false;
550 }
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.';
555 }
556 $missingCriteria = $this->passwordCriteriaMissing(
557 $password,
558 $passwordMinLength
559 );
560 if ($missingCriteria !== array()) {
561 $this->errors[] = 'Das Admin-Passwort erfüllt noch nicht: '
562 . implode(', ', $missingCriteria) . '.';
563 }
564 if ($this->errors !== array()) {
565 return false;
566 }
567
568 try {
569 $basics = $this->state('basics', array());
570 $admin = $this->installer()->ensureInitialAdmin(
571 true,
572 (string)($basics['default_lng'] ?? 'de'),
573 $password,
574 false
575 );
576 if (empty($admin['exists'])) {
577 $this->errors[] = 'Der persönliche Administratorzugang konnte nicht bereitgestellt werden.';
578 return false;
579 }
580 $created = !empty($admin['created']);
581 $reset = !empty($admin['reset']);
582 $this->setState('admin', array(
583 'email' => (string)($admin['email'] ?? ''),
584 'created' => $created ? 1 : 0,
585 'existing' => $created ? 0 : 1,
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,
591 ));
592 if ($created) {
593 $this->notices[] = 'Der Benutzer admin wurde mit dem persönlichen Installationspasswort angelegt.';
594 } else {
595 $this->notices[] = 'Das Passwort des vorhandenen Benutzers admin wurde durch das persönliche Installationspasswort ersetzt.';
596 }
597 return true;
598 } catch (Throwable $exception) {
599 $this->errors[] = $exception->getMessage();
600 return false;
601 }
602 }
603
604 private function processMail(): bool
605 {
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.';
609 return false;
610 }
611
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);
619 if ($password === '') {
620 $current = dbx()->get_config('dbx', 'mail', array());
621 $password = is_array($current)
622 ? (string)($current[self::SQL_SERVER]['pass'] ?? '')
623 : '';
624 }
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);
636 }
637 if ($shopFrom === '') {
638 $shopFrom = $this->moduleSenderAddress('shop', $fromEmail);
639 }
640
641 if ($mode === 'external') {
642 if (!in_array($transport, array('mail', 'smtp', 'sendmail'), true)) {
643 $this->errors[] = 'Der Mailtransport ist ungültig.';
644 }
645 if (!in_array($secure, array('', 'tls', 'ssl'), true)) {
646 $this->errors[] = 'Die SMTP-Verschlüsselung ist ungültig.';
647 }
648 if ($port < 1 || $port > 65535) {
649 $this->errors[] = 'Der SMTP-Port muss zwischen 1 und 65535 liegen.';
650 }
651 if (filter_var($fromEmail, FILTER_VALIDATE_EMAIL) === false) {
652 $this->errors[] = 'Für externen Versand ist eine gültige Absenderadresse erforderlich.';
653 }
654 if ($fromName === '') {
655 $this->errors[] = 'Für externen Versand ist ein Absendername erforderlich.';
656 }
657 if ($sender !== '' && filter_var($sender, FILTER_VALIDATE_EMAIL) === false) {
658 $this->errors[] = 'Der technische Envelope-Absender ist ungültig.';
659 }
660 if ($transport === 'smtp' && $host === '') {
661 $this->errors[] = 'Für SMTP ist ein Servername erforderlich.';
662 }
663 if ($transport === 'smtp' && $auth && ($user === '' || $password === '')) {
664 if ($user === '' || $password === '') {
665 $this->errors[] = 'Für SMTP-Authentifizierung werden Benutzer und Passwort benötigt.';
666 }
667 }
668 if ($sendTest
669 && filter_var($testRecipient, FILTER_VALIDATE_EMAIL) === false
670 ) {
671 $this->errors[] = 'Für die Test-E-Mail ist eine gültige Empfängeradresse erforderlich.';
672 }
673 } else {
674 // Deaktivierte HTML-Felder werden vom Browser nicht übertragen.
675 // Für den späteren Wechsel in den externen Modus bleiben deshalb
676 // technisch gültige, aber inaktive Profildefaults erhalten.
677 $transport = in_array($transport, array('mail', 'smtp', 'sendmail'), true)
678 ? $transport
679 : 'smtp';
680 $secure = in_array($secure, array('', 'tls', 'ssl'), true)
681 ? $secure
682 : 'tls';
683 $port = ($port >= 1 && $port <= 65535) ? $port : 587;
684 }
685 if (filter_var($contactFrom, FILTER_VALIDATE_EMAIL) === false) {
686 $this->errors[] = 'Der Absender für Kontaktanfragen ist ungültig.';
687 }
688 if (filter_var($shopFrom, FILTER_VALIDATE_EMAIL) === false) {
689 $this->errors[] = 'Der Absender für Shop-Nachrichten ist ungültig.';
690 }
691 if ($this->errors !== array()) {
692 return false;
693 }
694
695 $profile = array(
696 'transport' => $transport,
697 'host' => $host,
698 'port' => (string)$port,
699 'secure' => $secure,
700 'auth' => $auth ? '1' : '0',
701 'user' => $user,
702 'pass' => $password,
703 'from_email' => $fromEmail,
704 'from_name' => $fromName,
705 'from_domains' => $domains,
706 'sender' => $sender,
707 'force_from' => $forceFrom ? '1' : '0',
708 );
709 if (!dbx()->patch_local_config('dbx', array(
710 'mail_delivery_mode' => $mode,
711 'default_mail' => self::SQL_SERVER,
712 'mail' => array(self::SQL_SERVER => $profile),
713 ))) {
714 $this->errors[] = 'Die lokale E-Mail-Konfiguration konnte nicht gespeichert werden.';
715 return false;
716 }
717 if (!dbx()->patch_local_config('dbxContact', array(
718 'mail_profile' => self::SQL_SERVER,
719 'mail_from' => $contactFrom,
720 ))) {
721 $this->errors[] = 'Der Absender für Kontaktanfragen konnte nicht lokal gespeichert werden.';
722 return false;
723 }
724 if (!dbx()->patch_local_config('dbxShop', array(
725 'mail_profile' => self::SQL_SERVER,
726 'mail_from' => $shopFrom,
727 ))) {
728 $this->errors[] = 'Der Absender für Shop-Nachrichten konnte nicht lokal gespeichert werden.';
729 return false;
730 }
731
732 if ($mode === 'external' && $sendTest) {
733 $ok = dbx()->send_mail(
734 array('email' => $fromEmail, 'name' => $fromName),
735 $testRecipient,
736 'dbxapp Installation: E-Mail-Test',
737 '<p>Der E-Mail-Versand dieser dbxapp-Installation funktioniert.</p>',
738 'html',
739 array(),
740 array(
741 'text' => 'Der E-Mail-Versand dieser dbxapp-Installation funktioniert.',
742 'mail_profile' => self::SQL_SERVER,
743 )
744 );
745 if (!$ok) {
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 : '.');
750 return false;
751 }
752 $this->notices[] = 'Die Test-E-Mail wurde erfolgreich versendet.';
753 }
754
755 $this->setState('mail', array(
756 'mode' => $mode,
757 'transport' => $transport,
758 'host' => $host,
759 'port' => $port,
760 'secure' => $secure,
761 'auth' => $auth ? 1 : 0,
762 'user' => $user,
763 'from_email' => $fromEmail,
764 'from_name' => $fromName,
765 'sender' => $sender,
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,
772 'checked' => 1,
773 'checked_at' => time(),
774 ));
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.';
779 } else {
780 $this->notices[] = 'Der externe E-Mail-Versand ist konfiguriert.';
781 }
782 return true;
783 }
784
785 private function processFinish(): bool
786 {
787 if (!$this->postBool('confirm_install')) {
788 $this->errors[] = 'Bitte bestätigen Sie die Zusammenfassung vor dem Abschluss.';
789 return false;
790 }
791 if ((int)$this->state('completed', 0) < 6) {
792 $this->errors[] = 'Die notwendigen Installationsschritte sind noch nicht vollständig.';
793 return false;
794 }
795
796 $basics = $this->state('basics', array());
797 $admin = (array)$this->state('admin', array());
798 $config = dbx()->get_config('dbx');
799 $config = is_array($config) ? $config : array();
800 $secret = trim((string)($config['secure'] ?? ''));
801 if (strlen($secret) < 32) {
802 $secret = bin2hex(random_bytes(32));
803 }
804
805 $patch = array(
806 'install' => 0,
807 'ok' => 1,
808 'construct' => 0,
809 'secure' => $secret,
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(
819 6,
820 min(128, (int)($admin['password_min_length'] ?? 6))
821 ),
822 );
823 if (!dbx()->patch_local_config('dbx', $patch)) {
824 $this->errors[] = 'Der Installationsabschluss konnte nicht in config.local.php gespeichert werden.';
825 return false;
826 }
827
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()),
834 );
835 $this->finished = true;
836 $this->setState('completed', self::TOTAL_STEPS);
837 dbx()->invalidate_action_tokens();
838 return true;
839 }
840
841 private function renderStep(int $step): string
842 {
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(),
851 default => '',
852 };
853 }
854
855 private function renderSystem(): string
856 {
857 $checks = $this->systemChecks();
858 $requiredOk = true;
859 $rows = '';
860 foreach ($checks as $check) {
861 if ($check['required'] && $check['status'] !== 'ok') {
862 $requiredOk = false;
863 }
864 $icon = match ($check['status']) {
865 'ok' => 'bi-check-circle-fill',
866 'error' => 'bi-x-circle-fill',
867 default => 'bi-exclamation-triangle-fill',
868 };
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>'
874 . '</div>';
875 }
876
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>';
880
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')
888 . '</div>'
889 . '<div class="dbx-install-checks">' . $rows . '</div>'
890 . $this->formOpen(1)
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>'
893 . $button
894 . '</div></form>';
895 }
896
897 private function renderBasics(): string
898 {
899 $config = dbx()->get_config('dbx');
900 $config = is_array($config) ? $config : array();
901 $values = array_replace(
902 array(
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'),
910 ),
911 (array)$this->state('basics', array())
912 );
913 $designOptions = $this->selectOptions(
914 $this->designOptions(),
915 (string)$values['default_design_user']
916 );
917 $adminDesignOptions = $this->selectOptions(
918 $this->designOptions(),
919 (string)$values['default_design_admin']
920 );
921
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>'
925 . $this->formOpen(2)
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')
945 . '</div>'
946 . '</div></div></div>'
947 . $this->actions(2, 'Weiter zum Datenspeicher')
948 . '</form>';
949 }
950
951 private function renderDatabase(): string
952 {
953 $config = dbx()->get_config('dbx');
954 $config = is_array($config) ? $config : array();
955 $profile = is_array($config['db'][self::SQL_SERVER] ?? null)
956 ? $config['db'][self::SQL_SERVER]
957 : array();
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>'
964 . $this->formOpen(3)
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')
969 . '</div>'
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')
973 . '</div>'
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.'
983 )
984 . '</div></div>'
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(
991 array(
992 'mysql' => 'MySQL / MariaDB',
993 'pgsql' => 'PostgreSQL',
994 'sqlsrv' => 'Microsoft SQL Server',
995 ),
996 $dbType
997 )
998 . '</select></div>'
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>'
1004 . '</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.')
1007 . '</div>'
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(
1018 $saved,
1019 'Datenspeicher geprüft',
1020 match ($mode) {
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.',
1024 }
1025 )
1026 . $this->actionsWithCheck(
1027 3,
1028 'Weiter zu Strukturen & Daten',
1029 'check_database',
1030 !empty($saved['checked'])
1031 ? 'Datenbank erneut prüfen'
1032 : 'Datenbank prüfen'
1033 )
1034 . '</form>';
1035 }
1036
1037 private function renderSchema(): string
1038 {
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',
1045 );
1046 $mode = (string)($database['mode'] ?? 'sqlite');
1047 $migrate = !empty($database['migrate_data']);
1048 $bundled = $mode === 'sqlite';
1049
1050 return '<div class="dbx-install-section-head"><span class="dbx-install-kicker">Schritt 4</span>'
1051 . ($bundled
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>')
1054 . '</div>'
1055 . '<div class="dbx-install-plan">'
1056 . $this->planRow('bi-database', 'Zielspeicher', (string)($modeLabels[$mode] ?? $mode))
1057 . $this->planRow(
1058 'bi-table',
1059 'Data Definitions',
1060 count($catalog) . ($bundled ? ' ausgelieferte Tabellen werden geprüft' : ' Strukturen gefunden')
1061 )
1062 . $this->planRow(
1063 'bi-people',
1064 'Grunddaten',
1065 $bundled ? 'Mitgelieferte Datensätze bleiben unverändert' : 'Gast-, Mitglieder- und Admin-Gruppe'
1066 )
1067 . $this->planRow(
1068 'bi-arrow-left-right',
1069 'Datenübertragung',
1070 $migrate
1071 ? 'Vorhandene lokale Tabellen werden ausdrücklich übertragen'
1072 : ($bundled ? 'Nicht erforderlich' : 'Keine Übertragung vorhandener Daten')
1073 )
1074 . '</div>'
1075 . ($migrate
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>'
1077 : '')
1078 . $this->formOpen(4)
1079 . $this->actions(4, 'Weiter zur Administration')
1080 . '</form>';
1081 }
1082
1083 private function installerHelpButton(
1084 string $topic,
1085 string $label,
1086 string $icon
1087 ): string {
1088 $titles = array(
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',
1093 );
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
1098 . '|url=' . $url
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>';
1105 }
1106
1107 private function renderInstallerHelp(string $topic): string
1108 {
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>'
1117 . '</div>'
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>'
1123 . '</ol>'
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>'
1125 . '</article>';
1126 }
1127
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>'
1136 . '</div>'
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>'
1142 . '</ol>'
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>'
1144 . '</article>';
1145 }
1146
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>'
1158 . '</ol>'
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>'
1160 . '</article>';
1161 }
1162
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>'
1172 . '</div>'
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>'
1180 . '</ol>'
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 &amp; 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>'
1186 . '</div>'
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>'
1189 . '</article>';
1190 }
1191
1192 private function renderAdmin(): string
1193 {
1194 $config = dbx()->get_config('dbx');
1195 $config = is_array($config) ? $config : array();
1196 try {
1197 $admin = $this->installer()->inspectInitialAdmin();
1198 } catch (Throwable $exception) {
1199 $admin = array('exists' => false, 'default_password' => false);
1200 }
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);
1207 }
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')
1219 . '</div></div>'
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>'
1223 . ($adminExists
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')
1237 . '</form>';
1238 }
1239
1240 private function renderMail(): string
1241 {
1242 $config = dbx()->get_config('dbx');
1243 $config = is_array($config) ? $config : array();
1244 $profile = is_array($config['mail'][self::SQL_SERVER] ?? null)
1245 ? $config['mail'][self::SQL_SERVER]
1246 : array();
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']
1253 ?? ($profile['from_email'] ?? ($admin['email'] ?? '')));
1254 $contactConfig = dbx()->get_config('dbxContact');
1255 $contactConfig = is_array($contactConfig) ? $contactConfig : array();
1256 $shopConfig = dbx()->get_config('dbxShop');
1257 $shopConfig = is_array($shopConfig) ? $shopConfig : array();
1258 $contactDerived = $this->moduleSenderAddress('kontakt', $globalFrom);
1259 $contactFrom = (string)($saved['contact_from']
1260 ?? ($contactConfig['mail_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')
1265 ) {
1266 $contactFrom = $contactDerived;
1267 }
1268 $shopDerived = $this->moduleSenderAddress('shop', $globalFrom);
1269 $shopFrom = (string)($saved['shop_from']
1270 ?? ($shopConfig['mail_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')
1275 ) {
1276 $shopFrom = $shopDerived;
1277 }
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
1282 ) {
1283 $testRecipient = $globalFrom;
1284 }
1285
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')
1291 . '</div>'
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')
1297 . '</div>'
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">'
1317 . $this->input(
1318 'mail_password',
1319 'SMTP-Passwort',
1320 '',
1321 'password',
1322 false,
1323 !empty($profile['pass'])
1324 ? 'Leer lassen: vorhandenes Passwort beibehalten'
1325 : 'Wird lokal und nicht im Release gespeichert',
1326 'new-password'
1327 )
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>'
1347 . '</div></div>'
1348 . $this->switchInput('mail_force_from', 'Globalen Absender erzwingen', !empty($saved['force_from'] ?? $profile['force_from']), 'Nur wenn der Mailanbieter keine unterschiedlichen Absender erlaubt.')
1349 . '</section>'
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(
1355 $saved,
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.'
1360 )
1361 . $this->actionsWithCheck(
1362 6,
1363 'Weiter zur Abschlussprüfung',
1364 'check_mail',
1365 !empty($saved['checked'])
1366 ? 'E-Mail erneut prüfen'
1367 : 'E-Mail prüfen'
1368 )
1369 . '</form>'
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>';
1371 }
1372
1373 private function renderFinish(): string
1374 {
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',
1386 };
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',
1391 };
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'] ?? '')));
1396
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')
1408 . '</div>'
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>'
1414 . '</div></form>';
1415 }
1416
1417 private function renderShell(int $step, string $content): string
1418 {
1419 $alerts = '';
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>';
1422 }
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>';
1425 }
1426
1427 $completed = (int)$this->state('completed', 0);
1428 $nav = '';
1429 foreach ($this->steps as $number => $definition) {
1430 $class = $number === $step
1431 ? 'is-active'
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>'
1436 : (string)$number)
1437 . '</span><span class="dbx-install-step__text"><small>'
1438 . $this->h($definition['short'])
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>';
1442 } else {
1443 $nav .= '<span class="dbx-install-step ' . $class . '">' . $inner . '</span>';
1444 }
1445 }
1446 $progress = (int)round((($step - 1) / self::TOTAL_STEPS) * 100);
1447
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>'
1455 . '</aside>'
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">'
1460 . $alerts
1461 . '<div class="dbx-install-surface">' . $content . '</div>'
1462 . '</section></main></div>';
1463 }
1464
1465 private function renderFinished(): string
1466 {
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>'
1482 . '</div></div>';
1483 }
1484
1485 private function renderAlreadyInstalled(): string
1486 {
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>'
1492 . '</div></div>';
1493 }
1494
1495 private function formOpen(int $step, string $autocomplete = 'on'): string
1496 {
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))) . '">';
1499 }
1500
1501 private function actions(int $step, string $nextLabel): string
1502 {
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>';
1505 }
1506
1507 private function actionsWithCheck(
1508 int $step,
1509 string $nextLabel,
1510 string $checkAction,
1511 string $checkLabel
1512 ): string {
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>'
1517 . '</div></div>';
1518 }
1519
1523 private function checkedStatus(
1524 array $state,
1525 string $title,
1526 string $detail
1527 ): string {
1528 if (empty($state['checked'])) {
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>';
1532 }
1533
1534 $checkedAt = (int)($state['checked_at'] ?? 0);
1535 $when = $checkedAt > 0
1536 ? date('d.m.Y, H:i', $checkedAt) . ' Uhr'
1537 : 'in dieser Installationssitzung';
1538
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>';
1542 }
1543
1544 private function backLink(int $step): string
1545 {
1546 if ($step <= 1) {
1547 return '<span></span>';
1548 }
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>';
1550 }
1551
1552 private function fieldLabel(
1553 string $name,
1554 string $label,
1555 bool $required = false
1556 ): string {
1557 return '<label class="form-label" for="' . $this->h($name) . '">'
1558 . $this->h($label)
1559 . ($required ? ' <span aria-hidden="true">*</span>' : '')
1560 . $this->tooltipIcon($this->fieldTooltip($name))
1561 . '</label>';
1562 }
1563
1564 private function tooltipIcon(string $tooltip): string
1565 {
1566 if ($tooltip === '') {
1567 return '';
1568 }
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>';
1573 }
1574
1575 private function fieldTooltip(string $name): string
1576 {
1577 $tooltips = array(
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.',
1615 );
1616 return (string)($tooltips[$name] ?? '');
1617 }
1618
1619 private function input(
1620 string $name,
1621 string $label,
1622 string $value,
1623 string $type = 'text',
1624 bool $required = false,
1625 string $placeholder = '',
1626 string $autocomplete = '',
1627 bool $readonly = false
1628 ): string {
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' : '')
1633 . ($required ? ' required' : '') . '>';
1634 if ($type === 'password') {
1635 $input = '<div class="dbx-install-password-control">'
1636 . $input
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>';
1639 }
1640
1641 return '<div class="mb-3">' . $this->fieldLabel($name, $label, $required)
1642 . $input . '</div>';
1643 }
1644
1645 private function switchInput(
1646 string $name,
1647 string $label,
1648 bool $checked,
1649 string $hint
1650 ): string {
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>';
1656 }
1657
1658 private function choice(
1659 string $name,
1660 string $value,
1661 string $selected,
1662 string $icon,
1663 string $title,
1664 string $description,
1665 string $badge
1666 ): string {
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>';
1670 }
1671
1672 private function metric(string $label, string $value, string $icon): string
1673 {
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>';
1675 }
1676
1677 private function planRow(string $icon, string $label, string $value): string
1678 {
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>';
1680 }
1681
1682 private function summaryRow(
1683 string $icon,
1684 string $label,
1685 string $value,
1686 string $detail
1687 ): string {
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>';
1691 }
1692
1696 private function designOptions(): array
1697 {
1698 $root = dbx()->os_path(dbx()->get_base_dir() . 'dbx/design');
1699 $options = 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')
1704 ) {
1705 continue;
1706 }
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);
1713 if (is_array($metadata) && trim((string)($metadata['title'] ?? '')) !== '') {
1714 $title = trim((string)$metadata['title']);
1715 }
1716 }
1717 $options[$name] = $title;
1718 }
1719 if (!isset($options['dbxapp'])) {
1720 $options['dbxapp'] = 'dbxapp – Standard';
1721 }
1722 ksort($options, SORT_NATURAL | SORT_FLAG_CASE);
1723 return $options;
1724 }
1725
1726 private function selectOptions(array $options, string $selected): string
1727 {
1728 $html = '';
1729 foreach ($options as $value => $label) {
1730 $html .= '<option value="' . $this->h((string)$value) . '"'
1731 . ((string)$value === $selected ? ' selected' : '')
1732 . '>' . $this->h((string)$label) . '</option>';
1733 }
1734 return $html;
1735 }
1736
1737 private function installer(): dbxInstallationService
1738 {
1739 dbx()->get_include_obj('dbxInstallationService', 'dbxSetup');
1740 return new dbxInstallationService();
1741 }
1742
1743 private function requestedStep(): int
1744 {
1745 $value = $_GET['step'] ?? $_GET['stepp'] ?? $_POST['step'] ?? 1;
1746 $step = filter_var($value, FILTER_VALIDATE_INT);
1747 return max(1, min(self::TOTAL_STEPS, $step === false ? 1 : (int)$step));
1748 }
1749
1750 private function stepUrl(int $step): string
1751 {
1752 return '?dbx_modul=dbxSetup&dbx_run1=install&step='
1753 . max(1, min(self::TOTAL_STEPS, $step));
1754 }
1755
1756 private function isPost(): bool
1757 {
1758 return strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST';
1759 }
1760
1761 private function tokenScope(int $step): string
1762 {
1763 return 'dbxSetup.install.step.' . $step;
1764 }
1765
1766 private function validToken(int $step): bool
1767 {
1768 $token = is_scalar($_POST['install_token'] ?? null)
1769 ? (string)$_POST['install_token']
1770 : '';
1771 return dbx()->check_action_token($this->tokenScope($step), $token);
1772 }
1773
1774 private function state(string $key, $default = null)
1775 {
1776 return dbx()->get_session_var(
1777 $key,
1778 $default,
1779 self::SESSION_SECTION,
1780 self::SESSION_MODULE
1781 );
1782 }
1783
1784 private function setState(string $key, $value): void
1785 {
1786 dbx()->set_session_var(
1787 $key,
1788 $value,
1789 self::SESSION_SECTION,
1790 self::SESSION_MODULE
1791 );
1792 }
1793
1794 private function postText(string $name, int $maxLength): string
1795 {
1796 $value = is_scalar($_POST[$name] ?? null) ? trim((string)$_POST[$name]) : '';
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);
1801 }
1802
1803 private function postKey(string $name, int $maxLength): string
1804 {
1805 $value = $this->postText($name, $maxLength);
1806 return preg_match('/^[A-Za-z0-9_.\/:+-]*$/', $value) === 1 ? $value : '';
1807 }
1808
1809 private function postDatabaseName(string $name): string
1810 {
1811 $value = $this->postText($name, 64);
1812 return preg_match('/^[A-Za-z0-9_$-]+$/', $value) === 1 ? $value : '';
1813 }
1814
1815 private function postSecret(string $name, int $maxLength): string
1816 {
1817 $value = is_scalar($_POST[$name] ?? null) ? (string)$_POST[$name] : '';
1818 return substr(str_replace("\0", '', $value), 0, $maxLength);
1819 }
1820
1821 private function postInt(string $name, int $default): int
1822 {
1823 $value = filter_var($_POST[$name] ?? null, FILTER_VALIDATE_INT);
1824 return $value === false ? $default : (int)$value;
1825 }
1826
1827 private function postBool(string $name): bool
1828 {
1829 return isset($_POST[$name])
1830 && in_array(strtolower((string)$_POST[$name]), array('1', 'true', 'on', 'yes'), true);
1831 }
1832
1833 private function directoryWritable(string $directory): bool
1834 {
1835 if (!is_dir($directory)
1836 && !@mkdir($directory, 0775, true)
1837 && !is_dir($directory)
1838 ) {
1839 return false;
1840 }
1841 if (!is_writable($directory)) {
1842 return false;
1843 }
1844 $file = @tempnam($directory, 'dbx-inst-');
1845 if ($file === false) {
1846 return false;
1847 }
1848 $ok = @file_put_contents($file, 'ok', LOCK_EX) !== false
1849 && @file_get_contents($file) === 'ok';
1850 @unlink($file);
1851 return $ok;
1852 }
1853
1854 private function compactPath(string $path): string
1855 {
1856 $base = rtrim(str_replace('\\', '/', dbx()->get_base_dir()), '/') . '/';
1857 $normalized = str_replace('\\', '/', $path);
1858 return str_starts_with(strtolower($normalized), strtolower($base))
1859 ? './' . ltrim(substr($normalized, strlen($base)), '/')
1860 : basename($normalized);
1861 }
1862
1863 private function iniBytes(string $value): int
1864 {
1865 $value = strtolower(trim($value));
1866 if ($value === '-1') {
1867 return -1;
1868 }
1869 if ($value === '') {
1870 return 0;
1871 }
1872 $number = (float)$value;
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,
1878 };
1879 }
1880
1881 private function serverLabel(): string
1882 {
1883 $software = trim((string)($_SERVER['SERVER_SOFTWARE'] ?? 'PHP'));
1884 if ($software === '') {
1885 return PHP_SAPI;
1886 }
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);
1892 }
1893
1894 private function version(): string
1895 {
1896 $file = dbx()->os_path(dbx()->get_base_dir() . 'VERSION');
1897 return is_file($file) ? trim((string)file_get_contents($file)) : 'aktuell';
1898 }
1899
1900 private function mailModeLabel(string $mode): string
1901 {
1902 return match ($mode) {
1903 'external' => 'Extern',
1904 'disabled' => 'Aus',
1905 default => 'Nur intern',
1906 };
1907 }
1908
1909 private function moduleSenderAddress(string $localPart, string $globalFrom): string
1910 {
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, '@') ?: '',
1916 1
1917 ));
1918 if ($candidate !== '') {
1919 $domain = $candidate;
1920 }
1921 }
1922 return $localPart . '@' . $domain;
1923 }
1924
1928 private function passwordCriteriaMissing(
1929 string $password,
1930 int $minimumLength
1931 ): array {
1932 return \dbxPasswordPolicy::missingCriteria(
1933 $password,
1934 $minimumLength
1935 );
1936 }
1937
1938 private function h(string $value): string
1939 {
1940 return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1941 }
1942}
Geführte, wiederanlaufbare Erstinstallation von dbxapp.
systemChecks()
Liefert die Systemprüfungen auch für CLI-Tests und Statusanzeigen.
run()
Rendert und verarbeitet den aktuellen Installationsschritt.
Zentrale Passwortvorgaben für Installation, Login und Profilbearbeitung.
if($failed !==array()) $installer
dbx()
Liefert die zentrale dbXapp-API als Singleton.
Definition dbxApi.php:2805
get_base_dir(int $cutData=0)
Liefert das Basisverzeichnis der Installation.
Definition dbxApi.php:1507
get_file_dir()
Liefert das files/-Verzeichnis der Installation.
Definition dbxApi.php:1524
set_local_config_section(string $modul, string $section, array $value)
Ersetzt genau einen lokalen Konfigurationsbereich.
Definition dbxApi.php:896
action_token(string $scope='global')
Liefert ein sessiongebundenes Token fuer zustandsaendernde Link-Aktionen.
Definition dbxApi.php:1189
patch_local_config(string $modul, array $patch)
Fuehrt einen lokalen Konfigurationsausschnitt rekursiv zusammen.
Definition dbxApi.php:875
get_base_url()
Liefert die Basis-URL des aktuellen Requests.
Definition dbxApi.php:1488
$_SERVER['REQUEST_METHOD']
if($resolved !==$expectedBase . 'files/test/') $config
if(!is_string($cms)||!is_string($process)) $checks
if(!is_object( $db)) if($db->connect_db_server($server) !==1) $created
if(!defined( 'IMG_WEBP')) define( 'IMG_WEBP'
$https
Definition index.php:20
DBX schema administration.
$profile
Definition run.php:6
$titles
Definition update-seo.php:5