dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxInstallationService.class.php
Go to the documentation of this file.
1<?php
2
3declare(strict_types=1);
4
5namespace dbx\dbxSetup;
6
7use RuntimeException;
8
17{
18 private object $db;
19 private object $dd;
20
21 public function __construct(?object $db = null, ?object $dd = null)
22 {
23 $this->db = $db ?? dbx()->get_system_obj('dbxDB');
24 $this->dd = $dd ?? dbx()->get_system_obj('dbxDD');
25
26 if (!is_object($this->db) || !is_object($this->dd)) {
27 throw new RuntimeException('dbxDB/dbxDD stehen fuer die Installation nicht zur Verfuegung.');
28 }
29 }
30
43 public function discoverDDs(array $modules = array()): array
44 {
45 $moduleFilter = array();
46 foreach ($modules as $module) {
47 $module = trim((string)$module);
48 if ($module !== '') {
49 $moduleFilter[strtolower($module)] = true;
50 }
51 }
52
53 $root = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/');
54 $records = array();
55 foreach (glob($root . '*/dd/*.dd.php') ?: array() as $file) {
56 $normalized = str_replace('\\', '/', $file);
57 if (str_contains($normalized, '/_backup/')
58 || preg_match('#/dbx/modules/([^/]+)/dd/([^/]+)\.dd\.php$#', $normalized, $match) !== 1
59 ) {
60 continue;
61 }
62
63 $module = (string)$match[1];
64 $name = (string)$match[2];
65 if ($name === '' || str_starts_with($name, '.')) {
66 continue;
67 }
68 if ($moduleFilter !== array()
69 && !isset($moduleFilter[strtolower($module)])
70 ) {
71 continue;
72 }
73
74 $definition = $this->readDDTableDefinition($file);
75 $records[$module . '|' . $name] = array(
76 'module' => $module,
77 'name' => $name,
78 'dd' => $module . '|' . $name,
79 'file' => $file,
80 'declared_server' => trim((string)($definition['server'] ?? '')),
81 'table' => trim((string)($definition['table'] ?? '')),
82 );
83 }
84
85 uksort($records, static function (string $left, string $right): int {
86 $leftCore = str_starts_with($left, 'dbx|') ? 0 : 1;
87 $rightCore = str_starts_with($right, 'dbx|') ? 0 : 1;
88 return $leftCore <=> $rightCore ?: strcasecmp($left, $right);
89 });
90
91 return array_values($records);
92 }
93
99 private function readDDTableDefinition(string $file): array
100 {
101 if (!is_file($file) || !is_readable($file)) {
102 return array();
103 }
104
105 $definition = (static function (string $ddFile): array {
106 $table = array();
107 $fields = array();
108 $indexes = array();
109 $field = array();
110 $index = array();
111 include $ddFile;
112 return is_array($table) ? $table : array();
113 })($file);
114
115 return is_array($definition) ? $definition : array();
116 }
117
124 public function bindAllToServer(string $server, array $modules = array()): array
125 {
126 $server = trim($server);
127 if ($server === '') {
128 throw new RuntimeException('Der Zielserver fuer die DD-Bindungen fehlt.');
129 }
130
131 $bindings = array();
132 foreach ($this->discoverDDs($modules) as $record) {
133 $bindings[$record['dd']] = $server;
134 }
135
136 if (!dbx()->patch_local_config('dbx', array(
137 'dd_server_bindings' => $bindings,
138 ))) {
139 throw new RuntimeException('Lokale DD-Serverbindungen konnten nicht gespeichert werden.');
140 }
141
142 return $bindings;
143 }
144
150 public function provisionSchema(array $modules = array()): array
151 {
152 $results = array();
153 $errors = array();
154 $records = $this->discoverDDs($modules);
155
156 foreach ($records as $record) {
157 $binding = $this->db->get_dd_server_binding_info($record['dd']);
158 if (empty($binding['valid']) || ($binding['resolved_server'] ?? '') === '') {
159 $errors[] = $record['dd'] . ': ungueltige DD-Serverbindung';
160 continue;
161 }
162
163 $this->dd->sync_dd_to_db(
164 $record['module'],
165 $record['name'],
166 'reset'
167 );
168
169 $state = array();
170 for ($step = 0; $step < 1000; $step++) {
171 $state = $this->dd->sync_dd_to_db(
172 $record['module'],
173 $record['name'],
174 'apply'
175 );
176 $status = (string)($state['status'] ?? '');
177 if (in_array($status, array('finished', 'error', 'cancelled'), true)) {
178 break;
179 }
180 }
181
182 $results[$record['dd']] = array(
183 'server' => (string)$binding['resolved_server'],
184 'status' => (string)($state['status'] ?? 'error'),
185 'message' => (string)($state['message'] ?? ''),
186 );
187 if (($state['status'] ?? '') !== 'finished') {
188 $errors[] = $record['dd'] . ': '
189 . (string)($state['message'] ?? 'Schema-Synchronisation fehlgeschlagen');
190 }
191 }
192
193 return array(
194 'ok' => $errors === array(),
195 'total' => count($records),
196 'finished' => count($results) - count($errors),
197 'results' => $results,
198 'errors' => $errors,
199 );
200 }
201
211 public function verifyBundledSchema(array $modules = array()): array
212 {
213 if (!method_exists($this->dd, 'get_table_exist')) {
214 throw new RuntimeException('Die lesende DD-Strukturprüfung steht nicht zur Verfügung.');
215 }
216
217 $records = $this->discoverDDs($modules);
218 $results = array();
219 $errors = array();
220 $verified = 0;
221 foreach ($records as $record) {
222 $binding = $this->db->get_dd_server_binding_info($record['dd']);
223 $server = trim((string)($binding['resolved_server'] ?? ''));
224 $table = method_exists($this->db, 'get_dd_table')
225 ? trim((string)$this->db->get_dd_table($record['dd']))
226 : '';
227 if ($table === '') {
228 $table = trim((string)($record['table'] ?? ''));
229 }
230 $valid = !empty($binding['valid'])
231 && $server !== ''
232 && $table !== ''
233 && $this->dd->get_table_exist($server, $table);
234 $results[$record['dd']] = array(
235 'status' => $valid ? 'verified' : 'missing',
236 'server' => $server,
237 'table' => $table,
238 );
239 if ($valid) {
240 $verified++;
241 } else {
242 $errors[] = $record['dd']
243 . ': ausgelieferte DB3-Tabelle fehlt oder ist nicht erreichbar';
244 }
245 }
246
247 return array(
248 'ok' => $errors === array(),
249 'total' => count($records),
250 'verified' => $verified,
251 'results' => $results,
252 'errors' => $errors,
253 );
254 }
255
268 string $targetServer,
269 array $modules = array()
270 ): array {
271 $targetServer = trim($targetServer);
272 if ($targetServer === '') {
273 throw new RuntimeException('Der Zielserver für die Datenübertragung fehlt.');
274 }
275 if (!method_exists($this->dd, 'get_table_exist')
276 || !method_exists($this->dd, 'transfer_table')
277 ) {
278 throw new RuntimeException('Die DD-Datenübertragung steht nicht zur Verfügung.');
279 }
280
281 $results = array();
282 $errors = array();
283 $transferred = 0;
284 $skipped = 0;
285 $records = $this->discoverDDs($modules);
286
287 foreach ($records as $record) {
288 $ddRef = (string)$record['dd'];
289 $sourceServer = trim((string)($record['declared_server'] ?? ''));
290 $table = trim((string)($record['table'] ?? ''));
291 if ($sourceServer === ''
292 || $table === ''
293 || strcasecmp($sourceServer, $targetServer) === 0
294 || !$this->dd->get_table_exist($sourceServer, $table)
295 ) {
296 $results[$ddRef] = array(
297 'status' => 'skipped',
298 'source' => $sourceServer,
299 'target' => $targetServer,
300 'table' => $table,
301 );
302 $skipped++;
303 continue;
304 }
305
306 $this->dd->transfer_table(
307 $sourceServer,
308 $table,
309 $targetServer,
310 $table,
311 'reset',
312 0,
313 1
314 );
315 $state = array();
316 for ($step = 0; $step < 100000; $step++) {
317 $state = $this->dd->transfer_table(
318 $sourceServer,
319 $table,
320 $targetServer,
321 $table,
322 'step',
323 0,
324 1
325 );
326 if (in_array(
327 (string)($state['status'] ?? ''),
328 array('finished', 'error', 'cancelled'),
329 true
330 )) {
331 break;
332 }
333 }
334
335 $status = (string)($state['status'] ?? 'error');
336 $results[$ddRef] = array(
337 'status' => $status,
338 'source' => $sourceServer,
339 'target' => $targetServer,
340 'table' => $table,
341 'message' => (string)($state['message'] ?? ''),
342 );
343 if ($status === 'finished') {
344 $transferred++;
345 } else {
346 $errors[] = $ddRef . ': '
347 . (string)($state['message'] ?? 'Datenübertragung fehlgeschlagen');
348 }
349 }
350
351 return array(
352 'ok' => $errors === array(),
353 'total' => count($records),
354 'transferred' => $transferred,
355 'skipped' => $skipped,
356 'results' => $results,
357 'errors' => $errors,
358 );
359 }
360
365 public function seedCoreGroups(): array
366 {
367 $groups = array(
368 'guest' => 'Nicht angemeldete Benutzer',
369 'member' => 'Bestaetigte Benutzer',
370 'admin' => 'Systemadministratoren',
371 );
372 $result = array('created' => array(), 'existing' => array());
373
374 foreach ($groups as $name => $description) {
375 $existing = $this->db->select1(
376 'dbx|dbxUser_groups',
377 array('name' => $name),
378 array('id', 'name'),
379 0
380 );
381 if ((int)($existing['id'] ?? 0) > 0) {
382 $result['existing'][] = $name;
383 continue;
384 }
385
386 $id = $this->db->insert(
387 'dbx|dbxUser_groups',
388 array(
389 'name' => $name,
390 'description' => $description,
391 'active' => 1,
392 ),
393 0,
394 1,
395 1,
396 0
397 );
398 if ((int)$id <= 0) {
399 throw new RuntimeException('Systemgruppe konnte nicht angelegt werden: ' . $name);
400 }
401 $result['created'][] = $name;
402 }
403
404 return $result;
405 }
406
415 public function inspectInitialAdmin(): array
416 {
417 $existing = $this->db->select1(
418 'dbx|dbxUser',
419 array('uname' => 'admin'),
420 array('id', 'uname', 'pass', 'email', 'settings'),
421 0
422 );
423 if ((int)($existing['id'] ?? 0) <= 0) {
424 return array(
425 'exists' => false,
426 'id' => 0,
427 'email' => '',
428 'default_password' => false,
429 'password_reset_required' => false,
430 'created' => false,
431 'reset' => false,
432 );
433 }
434
435 $settings = json_decode((string)($existing['settings'] ?? ''), true);
436 return array(
437 'exists' => true,
438 'id' => (int)$existing['id'],
439 'email' => (string)($existing['email'] ?? ''),
440 'default_password' => password_verify(
441 '123456',
442 (string)($existing['pass'] ?? '')
443 ),
444 'password_reset_required' => is_array($settings)
445 && !empty($settings['password_reset_required']),
446 'created' => false,
447 'reset' => false,
448 );
449 }
450
459 public function ensureInitialAdmin(
460 bool $createIfMissing,
461 string $language,
462 string $password,
463 bool $passwordResetRequired = false
464 ): array {
465 if (strlen($password) < 6 || strlen($password) > 128) {
466 throw new RuntimeException('Das Admin-Passwort muss zwischen 6 und 128 Zeichen lang sein.');
467 }
468
469 $status = $this->inspectInitialAdmin();
470 if (!$status['exists'] && !$createIfMissing) {
471 return $status;
472 }
473
474 if (!$status['exists']) {
475 $id = $this->db->insert(
476 'dbx|dbxUser',
477 array(
478 'uname' => 'admin',
479 'pass' => password_hash($password, PASSWORD_DEFAULT),
480 'email' => '',
481 'roles' => 'admin',
482 'language' => preg_match('/^[a-z]{2,3}$/', $language)
483 ? $language
484 : 'de',
485 'status' => 1,
486 'is_confirm' => 1,
487 'settings' => json_encode(
488 $passwordResetRequired
489 ? array('password_reset_required' => 1)
490 : array('password_changed_at' => date(DATE_ATOM)),
491 JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
492 ),
493 ),
494 0,
495 1,
496 1,
497 0
498 );
499 if ((int)$id <= 0) {
500 throw new RuntimeException('Der initiale Admin-Benutzer konnte nicht angelegt werden.');
501 }
502
503 $status = $this->inspectInitialAdmin();
504 $status['created'] = true;
505 $status['reset'] = true;
506 return $status;
507 }
508
509 $existing = $this->db->select1(
510 'dbx|dbxUser',
511 array('uname' => 'admin'),
512 array('id', 'settings'),
513 0
514 );
515 $settings = json_decode((string)($existing['settings'] ?? ''), true);
516 $settings = is_array($settings) ? $settings : array();
517 if ($passwordResetRequired) {
518 $settings['password_reset_required'] = 1;
519 unset($settings['password_changed_at']);
520 } else {
521 unset($settings['password_reset_required']);
522 $settings['password_changed_at'] = date(DATE_ATOM);
523 }
524
525 $updated = $this->db->update(
526 'dbx|dbxUser',
527 array(
528 'pass' => password_hash($password, PASSWORD_DEFAULT),
529 'roles' => 'admin',
530 'status' => 1,
531 'is_confirm' => 1,
532 'settings' => json_encode(
533 $settings,
534 JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
535 ),
536 ),
537 (int)$status['id'],
538 0,
539 1,
540 1,
541 0
542 );
543 if (!$updated) {
544 throw new RuntimeException('Der vorhandene Admin-Benutzer konnte nicht auf den Installationsstandard zurückgesetzt werden.');
545 }
546
547 $status = $this->inspectInitialAdmin();
548 $status['reset'] = true;
549 return $status;
550 }
551
558 public function createAdmin(
559 string $password,
560 string $email,
561 string $language = 'de'
562 ): array {
563 $existing = $this->db->select1(
564 'dbx|dbxUser',
565 array('uname' => 'admin'),
566 array('id', 'uname'),
567 0
568 );
569 if ((int)($existing['id'] ?? 0) > 0) {
570 return array(
571 'created' => false,
572 'id' => (int)$existing['id'],
573 'reason' => 'admin_exists',
574 );
575 }
576
577 if (strlen($password) < 12) {
578 throw new RuntimeException('Das Admin-Passwort muss mindestens 12 Zeichen lang sein.');
579 }
580 if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
581 throw new RuntimeException('Die Admin-E-Mail-Adresse ist ungueltig.');
582 }
583
584 $id = $this->db->insert(
585 'dbx|dbxUser',
586 array(
587 'uname' => 'admin',
588 'pass' => password_hash($password, PASSWORD_DEFAULT),
589 'email' => $email,
590 'roles' => 'admin',
591 'language' => preg_match('/^[a-z]{2,3}$/', $language)
592 ? $language
593 : 'de',
594 'status' => 1,
595 'is_confirm' => 1,
596 'settings' => '{}',
597 ),
598 0,
599 1,
600 1,
601 0
602 );
603 if ((int)$id <= 0) {
604 throw new RuntimeException('Der Admin-Benutzer konnte nicht angelegt werden.');
605 }
606
607 return array('created' => true, 'id' => (int)$id, 'reason' => '');
608 }
609}
$table['server']
Definition .dd.php:6
$indexes[]
Definition .dd.php:167
if(!is_array( $before)||(int)( $before[ 'id'] ?? 0) !==$mediaId) if((string)($before['media_type'] ?? '') !=='video') $updated
Reproduzierbare Erstinstallation aus DDs und idempotenten Seeds.
bindAllToServer(string $server, array $modules=array())
Bindet alle gefundenen DDs auf einen konfigurierten Server.
discoverDDs(array $modules=array())
Liefert alle installierbaren DDs, optional begrenzt auf Module.
transferDeclaredDataToServer(string $targetServer, array $modules=array())
Überträgt vorhandene Daten aus den in den DDs deklarierten Quellspeichern auf einen neu konfigurierte...
inspectInitialAdmin()
Liefert den Zustand des initialen Administrators ohne Datenänderung.
ensureInitialAdmin(bool $createIfMissing, string $language, string $password, bool $passwordResetRequired=false)
Stellt bei der Installation den persönlichen Administratorzugang bereit.
createAdmin(string $password, string $email, string $language='de')
Erstellt den Admin nur bei einer echten Erstinstallation.
verifyBundledSchema(array $modules=array())
Prüft die mitgelieferten DB3-Tabellen ausschließlich lesend.
seedCoreGroups()
Legt die verbindlichen Core-Gruppen an, ohne bestehende Werte zu ueberschreiben.
__construct(?object $db=null, ?object $dd=null)
provisionSchema(array $modules=array())
Erstellt beziehungsweise ergaenzt die DD-Struktur ohne Force-Rebuild.
if((int)( $admin[ 'id'] ?? 0)<=0) if(!dbx() ->patch_local_config('dbxLogin', array('register'=> '0'))) $binding
get_base_dir(int $cutData=0)
Liefert das Basisverzeichnis der Installation.
Definition dbxApi.php:1507
patch_local_config(string $modul, array $patch)
Fuehrt einen lokalen Konfigurationsausschnitt rekursiv zusammen.
Definition dbxApi.php:875
if(!defined( 'IMG_WEBP')) define( 'IMG_WEBP'
DBX schema administration.
$results
Definition run_job.php:143