dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxSchema.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxAdmin;
3
4dbx()->get_system_obj('dbxReport', 'use');
5
23class dbxSchema extends \dbxObj {
24
25 private $schemaTexts;
26
30 private function schema_texts() {
31 if ($this->schemaTexts) {
32 return $this->schemaTexts;
33 }
34 dbx()->get_system_obj('dbxForm', 'use');
35 $texts = new \dbxForm();
36 $texts->set_form_help_enabled(false);
37 $texts->_fd = 'dbxAdmin|schema-report';
38 $texts->load_fd_messages();
39 $this->schemaTexts = $texts;
40 return $this->schemaTexts;
41 }
42
49 private function esc($value) {
50 return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
51 }
52
60 private function norm_key($server, $table) {
61 return strtolower((string) $server) . '|' . strtolower((string) $table);
62 }
63
71 private function encode_db_rid($server, $table) {
72 $json = json_encode(array((string)$server, (string)$table), JSON_UNESCAPED_SLASHES);
73 return 'db_' . rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
74 }
75
82 private function decode_db_rid($rid) {
83 $rid = (string)$rid;
84
85 if (str_starts_with($rid, 'db_')) {
86 $raw = substr($rid, 3);
87 $pad = strlen($raw) % 4;
88 if ($pad) {
89 $raw .= str_repeat('=', 4 - $pad);
90 }
91
92 $json = base64_decode(strtr($raw, '-_', '+/'), true);
93 $data = $json !== false ? json_decode($json, true) : null;
94 if (is_array($data) && count($data) >= 2) {
95 return array((string)$data[0], (string)$data[1]);
96 }
97 }
98
99 $parts = explode('|', $rid, 2);
100 return count($parts) == 2 ? $parts : array('', '');
101 }
102
110 private function dd_ref($modul, $dd) {
111 return ($modul && $modul != 'dbx') ? $modul . '|' . $dd : $dd;
112 }
113
120 private function path_rel($path) {
121 $path = str_replace('\\', '/', (string)$path);
122 $base = str_replace('\\', '/', dbx()->get_base_dir());
123
124 if ($base !== '' && str_starts_with($path, $base)) {
125 $path = substr($path, strlen($base));
126 }
127
128 $path = ltrim($path, '/');
129 if (str_starts_with($path, 'dbx/modules/')) {
130 $path = substr($path, strlen('dbx/modules/'));
131 }
132
133 return $path;
134 }
135
144 private function build_url($run1, $run2, $params = array()) {
145 $url = '?dbx_modul=dbxAdmin&dbx_run1=' . rawurlencode($run1) . '&dbx_run2=' . rawurlencode($run2);
146
147 foreach ($params as $key => $value) {
148 $url .= '&' . rawurlencode((string) $key) . '=' . rawurlencode((string) $value);
149 }
150
151 return $url;
152 }
153
165 private function openwin($url, $icon, $title, $width = 1200, $height = 780, $class = 'btn-inline') {
166 $titleEsc = $this->esc($title);
167 $urlEsc = $this->esc($url);
168
169 return '<a class="' . $class . ' dbx-win" href="' . $urlEsc . '" title="' . $titleEsc . '" '
170 . 'data-url="' . $urlEsc . '" data-title="' . $titleEsc
171 . '" role="button"><i class="' . $this->esc($icon) . '"></i></a>';
172 }
173
181 private function badge($label, $class = 'secondary') {
182 return '<span class="badge bg-' . $this->esc($class) . '">' . $this->esc($label) . '</span>';
183 }
184
192 private function header_icon($icon, $title) {
193 return '<i class="' . $this->esc($icon) . '" title="' . $this->esc($title) . '" aria-label="' . $this->esc($title) . '"></i>';
194 }
195
202 private function sanitize_dd_name($name) {
203 $name = preg_replace('/[^A-Za-z0-9_]+/', '_', (string) $name);
204 $name = trim($name, '_');
205
206 if ($name === '') {
207 $name = 'new_dd';
208 }
209
210 if (preg_match('/^[0-9]/', $name)) {
211 $name = 'dd_' . $name;
212 }
213
214 return $name;
215 }
216
223 private function sanitize_db_view_part($name) {
224 $name = preg_replace('/[^A-Za-z0-9_.-]+/', '_', (string) $name);
225 $name = trim($name, '._-');
226
227 if ($name === '') {
228 $name = 'db';
229 }
230
231 return $name;
232 }
233
240 private function backup_name_part($name) {
241 $name = preg_replace('/[^A-Za-z0-9_.-]+/', '_', (string)$name);
242 $name = trim($name, '._-');
243 return $name !== '' ? $name : 'db';
244 }
245
253 private function quote_db_ident($server, $name) {
254 $dbType = dbx()->get_system_obj('dbxDB')->get_db_type($server);
255 $name = str_replace(array('`', '"', ']'), '', (string)$name);
256
257 if ($dbType === 'mysql') {
258 return '`' . str_replace('`', '``', $name) . '`';
259 }
260
261 if ($dbType === 'sqlsrv') {
262 return '[' . str_replace(']', ']]', $name) . ']';
263 }
264
265 return '"' . str_replace('"', '""', $name) . '"';
266 }
267
275 private function sql_db_value($server, $value) {
276 if ($value === null) {
277 return 'NULL';
278 }
279
280 if (is_bool($value)) {
281 return $value ? '1' : '0';
282 }
283
284 if (is_array($value) || is_object($value)) {
285 $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
286 }
287
288 $oDB = dbx()->get_system_obj('dbxDB');
289 return "'" . $oDB->escape((string)$value, $server) . "'";
290 }
291
297 private function backup_dir() {
298 $dir = dbx()->os_path(dbx()->get_file_dir() . 'db/backup/');
299 if (!is_dir($dir)) {
300 @mkdir($dir, 0775, true);
301 }
302 return rtrim(str_replace('\\', '/', $dir), '/') . '/';
303 }
304
311 private function backup_file_path($file) {
312 $file = basename((string)$file);
313 if ($file === '' || !preg_match('/\.json$/i', $file)) {
314 return '';
315 }
316
317 $path = $this->backup_dir() . $file;
318 $realDir = realpath($this->backup_dir());
319 $realFile = realpath($path);
320
321 if (!$realDir || !$realFile) {
322 return '';
323 }
324
325 $realDir = rtrim(str_replace('\\', '/', $realDir), '/') . '/';
326 $realFile = str_replace('\\', '/', $realFile);
327
328 return str_starts_with($realFile, $realDir) ? $realFile : '';
329 }
330
337 private function backup_relative_file($file) {
338 return 'files/db/backup/' . basename((string)$file);
339 }
340
348 private function db_view_dd_name($server, $table) {
349 return $this->sanitize_db_view_part($server) . '.' . $this->sanitize_db_view_part($table);
350 }
351
358 private function split_dd_rid($rid) {
359 $parts = explode('|', (string)$rid, 2);
360 if (count($parts) != 2) {
361 return array('', '');
362 }
363
364 $modul = trim($parts[0]);
365 $dd = trim($parts[1]);
366
367 if (!preg_match('/^[A-Za-z0-9_.-]+$/', $modul) || !preg_match('/^[A-Za-z0-9_.-]+$/', $dd)) {
368 return array('', '');
369 }
370
371 return array($modul, $dd);
372 }
373
381 private function dd_file_path($modul, $dd) {
382 return dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/' . $modul . '/dd/' . $dd . '.dd.php');
383 }
384
392 private function dd_editor_file($modul, $dd) {
393 return dbx()->editor_file_path($this->dd_file_path($modul, $dd));
394 }
395
403 private function delete_dd_file($modul, $dd) {
404 $file = $this->dd_file_path($modul, $dd);
405 $base = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/' . $modul . '/dd/');
406 $realBase = realpath($base);
407 $realFile = realpath($file);
408
409 if (!$realBase || !$realFile || !str_starts_with(str_replace('\\', '/', $realFile), rtrim(str_replace('\\', '/', $realBase), '/') . '/')) {
410 return 0;
411 }
412
413 if (!is_file($realFile)) {
414 return 0;
415 }
416
417 $ok = @unlink($realFile) ? 1 : 0;
418 if ($ok) {
419 $oDD = dbx()->get_system_obj('dbxDD');
420 $oDD->clear_dd_cache($modul . '|' . $dd);
421 }
422
423 return $ok;
424 }
425
434 private function merge_auto_dd_fields($oldFields, $dbFields, $server = '') {
435 $oldByName = array();
436 foreach ((array)$oldFields as $field) {
437 $name = strtolower((string)($field['name'] ?? ''));
438 if ($name !== '') {
439 $oldByName[$name] = $field;
440 }
441 }
442
443 $oDD = dbx()->get_system_obj('dbxDD');
444 $dbType = $server ? $oDD->get_db_type($server) : '';
445
446 $fields = array();
447 foreach ((array)$dbFields as $field) {
448 $name = strtolower((string)($field['name'] ?? ''));
449 if ($name !== '' && isset($oldByName[$name])) {
450 $field = $oDD->merge_dd_field_with_db_field($oldByName[$name], $field, $dbType);
451 }
452
453 $fields[] = $field;
454 }
455
456 return $fields;
457 }
458
466 private function ensure_db_view_dd($server, $table) {
467 $oDD = dbx()->get_system_obj('dbxDD');
468 if (!$server || !$table || !$oDD->get_table_exist($server, $table)) {
469 return '';
470 }
471
472 $ddName = $this->db_view_dd_name($server, $table);
473 $ddRef = 'dbx|' . $ddName;
474 $old = file_exists($this->dd_file_path('dbx', $ddName)) ? $oDD->get_dd_model($ddRef) : array();
475
476 $tableMeta = is_array($old['table'] ?? null) ? $old['table'] : array();
477 $tableMeta['server'] = $server;
478 $tableMeta['table'] = $table;
479 $tableMeta['datadic'] = $ddName;
480
481 $fields = $this->merge_auto_dd_fields($old['fields'] ?? array(), $oDD->get_db_fields($server, $table), $server);
482 $indexes = $oDD->get_db_indexes($server, $table);
483
484 if (!$fields) {
485 return '';
486 }
487
488 return $oDD->save_dd('dbx', $ddName, $tableMeta, $fields, $indexes) ? $ddRef : '';
489 }
490
497 private function data_report_fields($ddRef) {
498 $oDD = dbx()->get_system_obj('dbxDD');
499 $fields = $oDD->get_dd_fields($ddRef);
500 $out = array();
501
502 foreach ((array)$fields as $field) {
503 $name = (string)($field['name'] ?? '');
504 if ($name === '') {
505 continue;
506 }
507
508 $label = trim((string)($field['label'] ?? ''));
509 $out[$name] = $label !== '' ? $label : $name;
510 }
511
512 return $out;
513 }
514
522 private function data_grid_cols($ddRef, $primary) {
523 $oDB = dbx()->get_system_obj('dbxDB');
524 $fields = $oDB->get_dd_fields($ddRef);
525 $cols = array();
526
527 foreach ((array)$fields as $field) {
528 $name = (string)($field['name'] ?? '');
529 if ($name === '' || str_starts_with($name, '_')) {
530 continue;
531 }
532
533 $label = trim((string)($field['label'] ?? ''));
534 $label = str_replace(array(':', '[', ']'), '-', $label ?: $name);
535 $fieldType = strtolower(trim((string)($field['type'] ?? '')));
536 $gridType = $oDB->map_dd_type_to_grid_type($fieldType ?: 'text');
537 $protect = isset($field['protect']) ? (string)$field['protect'] : '0';
538
539 if ($protect === '2') {
540 continue;
541 }
542
543 $flag = ($name === $primary || $protect === '1') ? 'p' : '';
544 $options = array();
545
546 if ($name === 'content' || in_array($fieldType, array('mediumtext', 'longtext'), true)) {
547 $options[] = 'editor=textarea';
548 $options[] = 'formatter=truncate';
549 $options[] = 'bigEditor=1';
550 $options[] = 'maxChars=180';
551 $options[] = 'width=420';
552 $options[] = 'minWidth=260';
553 }
554
555 $col = $name . '[' . $label . ']:' . $gridType;
556 if ($flag !== '') {
557 $col .= ':' . $flag;
558 }
559 if ($options) {
560 $col .= ':' . implode(';', $options);
561 }
562
563 $cols[] = $col;
564 }
565
566 return implode(',', $cols);
567 }
568
574 private function data_context_from_request() {
575 $mode = dbx()->get_modul_var('dbx_run1', 'dd', 'parameter');
576 $rid = dbx()->get_modul_var('rid', '', 'parameter');
577 $ddRef = '';
578
579 if ($mode == 'db') {
580 [$server, $table] = $this->decode_db_rid($rid);
581 if ($server && $table) {
582 $ddRef = $this->ensure_db_view_dd($server, $table);
583 }
584 } else {
585 [$modul, $dd] = $this->split_dd_rid($rid);
586 if (!$modul || !$dd) {
587 [$modul, $dd] = $this->dd_params_from_request();
588 }
589 if ($modul && $dd) {
590 $ddRef = $this->dd_ref($modul, $dd);
591 }
592 }
593
594 $oDB = dbx()->get_system_obj('dbxDB');
595 $flds = $ddRef ? $this->data_report_fields($ddRef) : array();
596 $primary = $ddRef ? $oDB->get_dd_primary($ddRef) : 'id';
597
598 if (!$primary || !isset($flds[$primary])) {
599 $primary = isset($flds['id']) ? 'id' : '';
600 }
601
602 return array($ddRef, $flds, $primary);
603 }
604
613 private function data_row_by_id($ddRef, $primary, $id) {
614 if (!$ddRef || !$primary || $id === '' || $id === null) {
615 return array();
616 }
617
618 $oDB = dbx()->get_system_obj('dbxDB');
619 $rows = $oDB->select($ddRef, array($primary => $id), '', '', 'ASC', '', 1, 0, 0);
620 if (is_array($rows) && isset($rows[0]) && is_array($rows[0])) {
621 return $rows[0];
622 }
623
624 return array();
625 }
626
638 private function run_data_report($ddRef, $title, $readUrl, $saveUrl, $insertUrl, $deleteUrl) {
639 $oDB = dbx()->get_system_obj('dbxDB');
640 $oReport = dbx()->get_system_obj('dbxReport');
641 $flds = $this->data_report_fields($ddRef);
642 $primary = $oDB->get_dd_primary($ddRef);
643
644 if (!$flds || !$primary || !isset($flds[$primary])) {
645 return '<div class="alert alert-warning">Keine editierbare Felddefinition mit Primaerschluessel vorhanden.</div>';
646 }
647
648 $oReport->init('report-schema-data-' . substr(md5($ddRef), 0, 10), 'schema-data-report');
649 $oReport->_rflds = $flds;
650 $oReport->_mode = 'tabulurator';
651 $oReport->_rrows = 'auto';
652 $oReport->_grid_id = 'schema_data_' . substr(md5($ddRef), 0, 10);
653 $oReport->_grid_cols = $this->data_grid_cols($ddRef, $primary);
654 $oReport->_grid_layout = 'fitData';
655 $oReport->_grid_read_url = $readUrl;
656 $oReport->_grid_save_url = $saveUrl;
657 $oReport->_grid_insert_url = $insertUrl;
658 $oReport->_grid_delete_url = $deleteUrl;
659 $oReport->add_obj('title', 'obj-value', $this->esc($title));
660 $oReport->add_obj('subtitle', 'obj-value', $this->esc('Direkt editierbares Grid'));
661 $oReport->add_rep('bar_title', $title);
662 $oReport->add_rep('bar_subtitle', 'Direkt editierbares Grid');
663 $oReport->add_obj('primary', 'obj-value', $this->esc($primary));
664
665 return $oReport->run();
666 }
667
673 private function run_data_read() {
674 [$ddRef, $flds, $primary] = $this->data_context_from_request();
675 if (!$ddRef || !$flds || !$primary) {
676 $this->json_response(array('ok' => 0, 'count' => 0, 'rows' => array(), 'msg' => 'Keine Felddefinition vorhanden.'));
677 }
678
679 $oDB = dbx()->get_system_obj('dbxDB');
680 $rows = $oDB->select($ddRef, '', array_keys($flds), '', 'ASC', '', 0, 0, 0);
681 if (!is_array($rows)) {
682 $rows = array();
683 }
684
685 $out = array();
686 foreach (array_values($rows) as $pos => $row) {
687 if (is_array($row)) {
688 $row['_dbx_rownum'] = $pos + 1;
689 $out[] = $row;
690 }
691 }
692
693 $count = $oDB->count($ddRef, '');
694 $this->json_response(array(
695 'ok' => 1,
696 'count' => $count >= 0 ? $count : count($out),
697 'rows' => $out,
698 'server_time' => date('Y-m-d H:i:s'),
699 ));
700 }
701
707 private function run_data_save() {
708 [$ddRef, $flds, $primary] = $this->data_context_from_request();
709 $payload = $this->request_json();
710 $rows = is_array($payload['rows'] ?? null) ? $payload['rows'] : array();
711
712 if (!$ddRef || !$flds || !$primary) {
713 $this->json_response(array('ok' => 0, 'msg' => 'Keine editierbare Felddefinition vorhanden.'));
714 }
715
716 $oDB = dbx()->get_system_obj('dbxDB');
717 $okCount = 0;
718
719 foreach ($rows as $row) {
720 if (!is_array($row)) {
721 continue;
722 }
723
724 $id = $row[$primary] ?? ($row['id'] ?? null);
725 if ($id === null || $id === '') {
726 continue;
727 }
728
729 $values = array();
730 foreach ($flds as $field => $label) {
731 if ($field === $primary || !array_key_exists($field, $row)) {
732 continue;
733 }
734 $values[$field] = $row[$field];
735 }
736
737 if (!$values) {
738 continue;
739 }
740
741 $ok = $oDB->update($ddRef, $values, array($primary => $id), 1, 1, 1, 1);
742 if ($ok === 1 && $oDB->_update_count > 0) {
743 $okCount++;
744 }
745 }
746
747 $this->json_response(array('ok' => 1, 'success' => true, 'count' => $okCount));
748 }
749
755 private function run_data_insert() {
756 [$ddRef, $flds, $primary] = $this->data_context_from_request();
757 if (!$ddRef || !$flds || !$primary) {
758 $this->json_response(array('ok' => 0, 'msg' => 'Keine editierbare Felddefinition vorhanden.'));
759 }
760
761 $oDB = dbx()->get_system_obj('dbxDB');
762 $id = 0;
763 if ($oDB->insert($ddRef, array(), 1, 1, 1, 1) === 1) {
764 $id = $oDB->get_insert_id();
765 }
766 if ($id <= 0) {
767 $this->json_response(array('ok' => 0, 'msg' => 'Datensatz konnte nicht angelegt werden.'));
768 }
769
770 $row = $this->data_row_by_id($ddRef, $primary, $id);
771 $this->json_response(array(
772 'ok' => 1,
773 'success' => true,
774 'row' => $row,
775 ));
776 }
777
783 private function run_data_delete() {
784 [$ddRef, $flds, $primary] = $this->data_context_from_request();
785 $payload = $this->request_json();
786 $id = $payload['id'] ?? null;
787
788 if (!$ddRef || !$flds || !$primary || $id === null || $id === '') {
789 $this->json_response(array('ok' => 0, 'msg' => 'Datensatz-ID fehlt.'));
790 }
791
792 $oDB = dbx()->get_system_obj('dbxDB');
793 $ok = $oDB->delete($ddRef, array($primary => $id), 1, 1);
794 $this->json_response(array('ok' => $ok > 0 ? 1 : 0, 'success' => $ok > 0));
795 }
796
803 private function json_response($data) {
804 header('Content-Type: application/json; charset=utf-8');
805 echo json_encode($data, JSON_UNESCAPED_UNICODE);
806 exit;
807 }
808
814 private function request_json() {
815 $raw = file_get_contents('php://input');
816 $data = $raw ? json_decode($raw, true) : array();
817 return is_array($data) ? $data : array();
818 }
819
825 private function dd_params_from_request() {
826 $modul = dbx()->get_modul_var('modul', '', 'parameter');
827 $dd = dbx()->get_modul_var('dd', '', 'parameter');
828
829 if ((!$modul || !$dd) && strpos((string)$dd, '|') !== false) {
830 [$modul, $dd] = $this->split_dd_rid($dd);
831 }
832
833 if (!$modul && $dd) {
834 $modul = 'dbx';
835 }
836
837 if (!preg_match('/^[A-Za-z0-9_.-]+$/', (string)$modul) || !preg_match('/^[A-Za-z0-9_.-]+$/', (string)$dd)) {
838 return array('', '');
839 }
840
841 return array($modul, $dd);
842 }
843
849 private function dd_field_grid_keys() {
850 $oDD = dbx()->get_system_obj('dbxDD');
851 return $oDD->dd_field_schema_keys();
852 }
853
861 private function dd_field_grid_row($field, $id) {
862 $row = array(
863 'id' => (string)$id,
864 '_pos' => (int)$id + 1,
865 );
866
867 foreach ($this->dd_field_grid_keys() as $key) {
868 $value = $field[$key] ?? '';
869 $row[$key] = is_array($value) ? implode(',', $value) : (string)$value;
870 }
871
872 return $row;
873 }
874
881 private function dd_field_grid_rows($fields) {
882 $rows = array();
883 foreach (array_values((array)$fields) as $id => $field) {
884 $rows[] = $this->dd_field_grid_row((array)$field, $id);
885 }
886
887 return $rows;
888 }
889
895 private function dd_field_grid_cols() {
896 $types = implode('~', array(
897 'varchar', 'char', 'text', 'mediumtext', 'longtext',
898 'int', 'integer', 'bigint', 'smallint', 'mediumint', 'tinyint',
899 'decimal', 'float', 'double',
900 'date', 'datetime', 'timestamp', 'time',
901 'bool', 'boolean', 'json', 'blob',
902 ));
903
904 return implode(',', array(
905 '_pos[#]:number:p:width=62;hozAlign=center;headerHozAlign=center',
906 'name[Name]:text::width=150',
907 'type[Type]:text::editor=list;values=' . $types . ';width=120',
908 'index[Index]:text::editor=list;values==ohne~PRI=Primaer~MU=Mehrfach~INDEX=Index~UNIQUE=Unique;width=115',
909 'length[Laenge]:text::width=88',
910 'default[Default]:text::width=120',
911 'label[Label]:text::width=160',
912 'rules[Regeln]:text::width=180',
913 'tooltip[Tooltip]:text::editor=textarea;width=220',
914 'errormsg[Fehlermeldung]:text::editor=textarea;width=220',
915 'placeholder[Platzhalter]:text::width=160',
916 'convert[Convert]:text::width=120',
917 'protect[Schutz]:text::editor=list;values=0=editierbar~1=geschuetzt~2=versteckt;width=120',
918 'group[Gruppe]:text::width=130',
919 'mask[Maske]:text::width=130',
920 'data[Data]:text::editor=textarea;width=220',
921 'options[Optionen]:text::editor=textarea;width=220',
922 'tpl[TPL]:text::width=160',
923 'js[JS]:text::editor=textarea;width=180',
924 'prompt[Prompt]:text::editor=textarea;width=220',
925 ));
926 }
927
935 private function dd_field_model($modul, $dd) {
936 if (!$modul || !$dd) {
937 return array();
938 }
939
940 $oDD = dbx()->get_system_obj('dbxDD');
941 return $oDD->get_dd_model($this->dd_ref($modul, $dd));
942 }
943
953 private function save_dd_field_model($modul, $dd, $model, $fields) {
954 $oDD = dbx()->get_system_obj('dbxDD');
955 return $oDD->save_dd($modul, $dd, $model['table'] ?? array(), array_values($fields), $model['indexes'] ?? array());
956 }
957
965 private function normalize_dd_field_grid_post($row, $old = array()) {
966 $field = is_array($old) ? $old : array();
967
968 foreach ($this->dd_field_grid_keys() as $key) {
969 if (array_key_exists($key, $row)) {
970 $field[$key] = is_array($row[$key]) ? implode(',', $row[$key]) : trim((string)$row[$key]);
971 }
972 }
973
974 $field['name'] = trim((string)($field['name'] ?? ''));
975 $field['type'] = strtolower(trim((string)($field['type'] ?? 'varchar')));
976
977 if ($field['type'] === '') {
978 $field['type'] = 'varchar';
979 }
980
981 if (!isset($field['protect']) || !in_array((string)$field['protect'], array('0', '1', '2'), true)) {
982 $field['protect'] = '0';
983 }
984
985 return $field;
986 }
987
995 private function validate_dd_fields($fields, &$message = '') {
996 $names = array();
997
998 foreach ((array)$fields as $field) {
999 $name = trim((string)($field['name'] ?? ''));
1000 if ($name === '' || !preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name)) {
1001 $message = 'Ungueltiger Feldname: ' . $name;
1002 return false;
1003 }
1004
1005 $key = strtolower($name);
1006 if (isset($names[$key])) {
1007 $message = 'Feldname doppelt: ' . $name;
1008 return false;
1009 }
1010
1011 $names[$key] = true;
1012 }
1013
1014 return true;
1015 }
1016
1022 private function run_dd_fields_grid() {
1023 [$modul, $dd] = $this->dd_params_from_request();
1024 $model = $this->dd_field_model($modul, $dd);
1025
1026 if (!$model) {
1027 return '<div class="alert alert-danger">DD nicht gefunden.</div>';
1028 }
1029
1030 $baseParams = array(
1031 'modul' => $modul,
1032 'dd' => $dd,
1033 );
1034
1035 $oReport = dbx()->get_system_obj('dbxReport');
1036 $oReport->init('report-dd-fields-grid', 'schema-fields-grid');
1037 $oReport->_mode = 'tabulurator';
1038 $oReport->_rrows = 620;
1039 $oReport->_grid_id = 'dd_fields_' . substr(md5($modul . '|' . $dd), 0, 10);
1040 $oReport->_grid_cols = $this->dd_field_grid_cols();
1041 $oReport->_grid_layout = 'fitDataStretch';
1042 $oReport->_grid_read_url = $this->build_url('dd', 'fields_read', $baseParams);
1043 $oReport->_grid_save_url = $this->build_url('dd', 'fields_save', $baseParams);
1044 $oReport->_grid_insert_url = $this->build_url('dd', 'fields_insert', $baseParams);
1045 $oReport->_grid_delete_url = $this->build_url('dd', 'fields_delete', $baseParams);
1046 $oReport->add_rep('bar_title', 'DD-Felder: ' . $modul . '|' . $dd);
1047 $oReport->add_rep('bar_subtitle', $this->path_rel($this->dd_file_path($modul, $dd)));
1048
1049 return $oReport->run();
1050 }
1051
1057 private function run_dd_fields_read() {
1058 [$modul, $dd] = $this->dd_params_from_request();
1059 $model = $this->dd_field_model($modul, $dd);
1060
1061 if (!$model) {
1062 $this->json_response(array('ok' => 0, 'rows' => array(), 'msg' => 'DD nicht gefunden'));
1063 }
1064
1065 $rows = $this->dd_field_grid_rows($model['fields'] ?? array());
1066 $this->json_response(array(
1067 'ok' => 1,
1068 'count' => count($rows),
1069 'rows' => $rows,
1070 'server_time' => date('Y-m-d H:i:s'),
1071 ));
1072 }
1073
1079 private function run_dd_fields_save() {
1080 [$modul, $dd] = $this->dd_params_from_request();
1081 $model = $this->dd_field_model($modul, $dd);
1082 $payload = $this->request_json();
1083 $rows = is_array($payload['rows'] ?? null) ? $payload['rows'] : array();
1084
1085 if (!$model) {
1086 $this->json_response(array('ok' => 0, 'msg' => 'DD nicht gefunden'));
1087 }
1088
1089 $fields = array_values((array)($model['fields'] ?? array()));
1090 foreach ($rows as $row) {
1091 $id = isset($row['id']) ? (int)$row['id'] : -1;
1092 if ($id < 0 || !isset($fields[$id])) {
1093 continue;
1094 }
1095
1096 $fields[$id] = $this->normalize_dd_field_grid_post($row, $fields[$id]);
1097 }
1098
1099 $message = '';
1100 if (!$this->validate_dd_fields($fields, $message)) {
1101 $this->json_response(array('ok' => 0, 'msg' => $message));
1102 }
1103
1104 $ok = $this->save_dd_field_model($modul, $dd, $model, $fields);
1105 $this->json_response(array('ok' => $ok ? 1 : 0, 'success' => $ok ? true : false, 'rows' => $this->dd_field_grid_rows($fields)));
1106 }
1107
1113 private function run_dd_fields_insert() {
1114 [$modul, $dd] = $this->dd_params_from_request();
1115 $model = $this->dd_field_model($modul, $dd);
1116
1117 if (!$model) {
1118 $this->json_response(array('ok' => 0, 'msg' => 'DD nicht gefunden'));
1119 }
1120
1121 $fields = array_values((array)($model['fields'] ?? array()));
1122 $used = array();
1123 foreach ($fields as $field) {
1124 $used[strtolower((string)($field['name'] ?? ''))] = true;
1125 }
1126
1127 $name = 'new_field';
1128 $i = 1;
1129 while (isset($used[strtolower($name)])) {
1130 $i++;
1131 $name = 'new_field_' . $i;
1132 }
1133
1134 $oDD = dbx()->get_system_obj('dbxDD');
1135 $field = $oDD->normalize_dd_field(array(
1136 'name' => $name,
1137 'type' => 'varchar',
1138 'length' => '255',
1139 'index' => '',
1140 'label' => str_replace('_', ' ', ucfirst($name)),
1141 'rules' => '',
1142 'tpl' => 'text-label',
1143 ));
1144
1145 $fields[] = $field;
1146 $ok = $this->save_dd_field_model($modul, $dd, $model, $fields);
1147 $this->json_response(array(
1148 'ok' => $ok ? 1 : 0,
1149 'success' => $ok ? true : false,
1150 'row' => $this->dd_field_grid_row($field, count($fields) - 1),
1151 ));
1152 }
1153
1159 private function run_dd_fields_delete() {
1160 [$modul, $dd] = $this->dd_params_from_request();
1161 $model = $this->dd_field_model($modul, $dd);
1162 $payload = $this->request_json();
1163
1164 if (!$model) {
1165 $this->json_response(array('ok' => 0, 'msg' => 'DD nicht gefunden'));
1166 }
1167
1168 if (!array_key_exists('id', $payload)) {
1169 $this->json_response(array('ok' => 0, 'msg' => 'Feld-ID fehlt'));
1170 }
1171
1172 $id = (int)$payload['id'];
1173 $fields = array_values((array)($model['fields'] ?? array()));
1174 if ($id < 0 || !isset($fields[$id])) {
1175 $this->json_response(array('ok' => 0, 'msg' => 'Feld-ID ungueltig'));
1176 }
1177
1178 array_splice($fields, $id, 1);
1179 $ok = $this->save_dd_field_model($modul, $dd, $model, $fields);
1180 $this->json_response(array('ok' => $ok ? 1 : 0, 'success' => $ok ? true : false));
1181 }
1182
1188 private function get_module_options() {
1189 $options = array();
1190 $base = str_replace('\\', '/', dbx()->get_base_dir()) . 'dbx/modules/*';
1191
1192 foreach (glob($base, GLOB_ONLYDIR) as $dir) {
1193 $modul = basename($dir);
1194 if (is_dir($dir . '/dd')) {
1195 $options[$modul] = $modul;
1196 }
1197 }
1198
1199 if (!isset($options['dbx'])) {
1200 $options['dbx'] = 'dbx';
1201 }
1202
1203 ksort($options);
1204 return $options;
1205 }
1206
1212 private function get_server_options() {
1213 $options = array();
1214 $config = dbx()->get_config('dbx', 'db');
1215 $moduleFiles = $this->get_module_db_files();
1216 $moduleFileIndex = array();
1217
1218 foreach ($moduleFiles as $server => $db) {
1219 $options[$server] = $db['label'];
1220
1221 $real = realpath((string)($db['file'] ?? ''));
1222 if ($real) {
1223 $moduleFileIndex[strtolower(str_replace('\\', '/', $real))] = 1;
1224 }
1225 }
1226
1227 if (is_array($config)) {
1228 foreach ($config as $server => $data) {
1229 if (isset($options[$server])) {
1230 continue;
1231 }
1232
1233 $type = strtolower((string)($data['type'] ?? ''));
1234 $host = (string)($data['host'] ?? '');
1235 $name = (string)($data['dbname'] ?? ($data['name'] ?? ''));
1236 $isSqlite = ($type == 'sqlite' || $type == 'sqlite3' || preg_match('/\.(db3|sqlite|sqlite3)$/i', $name));
1237
1238 if ($isSqlite && ($host !== '' || $name !== '')) {
1239 $file = dbx()->os_path($host . $name);
1240 $real = realpath($file);
1241
1242 if ($real) {
1243 $real = strtolower(str_replace('\\', '/', $real));
1244 if (isset($moduleFileIndex[$real])) {
1245 continue;
1246 }
1247 }
1248 }
1249
1251 }
1252 }
1253
1254 return $options;
1255 }
1256
1262
1263
1264 private function get_module_db_files() {
1265 $records = array();
1266 $base = str_replace('\\', '/', dbx()->get_base_dir()) . 'dbx/modules/*/db/*';
1267
1268 foreach (glob($base) as $file) {
1269 if (!is_file($file) || !preg_match('/\.(db3|sqlite|sqlite3)$/i', $file)) {
1270 continue;
1271 }
1272
1273 $norm = str_replace('\\', '/', $file);
1274 if (!preg_match('#/dbx/modules/([^/]+)/db/([^/]+)$#', $norm, $match)) {
1275 continue;
1276 }
1277
1278 $modul = $match[1];
1279 $name = $match[2];
1280 $server = ($modul == 'dbx') ? $name : $modul . '|' . $name;
1281
1282 $records[$server] = array(
1283 'server' => $server,
1284 'modul' => $modul,
1285 'name' => $name,
1286 'file' => $norm,
1287 'path' => $this->path_rel($norm),
1288 'label' => $this->path_rel($norm),
1289 );
1290 }
1291
1292 ksort($records);
1293 return $records;
1294 }
1295
1302 private function resolve_module_db_file($server) {
1303 $server = (string)$server;
1304 if (!preg_match('/\.(db3|sqlite|sqlite3)$/i', $server)) {
1305 return '';
1306 }
1307
1308 $modul = 'dbx';
1309 $name = $server;
1310
1311 if (strpos($server, '|') !== false) {
1312 $parts = explode('|', $server, 2);
1313 $modul = trim($parts[0]) ?: 'dbx';
1314 $name = trim($parts[1]);
1315 }
1316
1317 $file = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/' . $modul . '/db/' . $name);
1318 if (file_exists($file)) {
1319 return str_replace('\\', '/', $file);
1320 }
1321
1322 if ($modul != 'dbx') {
1323 $file = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/dbx/db/' . $name);
1324 if (file_exists($file)) {
1325 return str_replace('\\', '/', $file);
1326 }
1327 }
1328
1329 return '';
1330 }
1331
1338 private function get_database_label($server) {
1339 $moduleFile = $this->resolve_module_db_file($server);
1340 if ($moduleFile) {
1341 return $this->path_rel($moduleFile);
1342 }
1343
1344 $config = dbx()->get_config('dbx', 'db');
1345
1346 if (is_array($config) && isset($config[$server])) {
1347 $db = $config[$server];
1348 $name = $db['dbname'] ?? ($db['name'] ?? '');
1349 $host = $db['host'] ?? '';
1350 $type = strtolower((string)($db['type'] ?? ''));
1351
1352 if ($name && $host) {
1353 $full = dbx()->os_path($host . $name);
1354 if ($type == 'sqlite' || preg_match('/[\/\\\\]/', (string)$host)) {
1355 return $this->path_rel($full);
1356 }
1357
1358 return rtrim((string)$host, '/') . '/' . $name;
1359 }
1360
1361 if ($name) {
1362 return $name;
1363 }
1364
1365 if ($host) {
1366 return $host;
1367 }
1368 }
1369
1370 return '';
1371 }
1372
1379 private function get_database_path_label($server) {
1380 $moduleFile = $this->resolve_module_db_file($server);
1381 if ($moduleFile) {
1382 return $this->path_rel($moduleFile);
1383 }
1384
1385 $config = dbx()->get_config('dbx', 'db');
1386 if (!is_array($config) || !isset($config[$server])) {
1387 return '';
1388 }
1389
1390 $db = $config[$server];
1391 $type = strtolower((string)($db['type'] ?? ''));
1392 $host = (string)($db['host'] ?? '');
1393 $name = (string)($db['dbname'] ?? ($db['name'] ?? ''));
1394
1395 if ($type == 'sqlite' && ($host !== '' || $name !== '')) {
1396 return $this->path_rel(dbx()->os_path($host . $name));
1397 }
1398
1399 return '';
1400 }
1401
1408
1409 private function get_server_module($server) {
1410 $server = (string)$server;
1411 static $moduleFiles = null;
1412 if ($moduleFiles === null) {
1413 $moduleFiles = $this->get_module_db_files();
1414 }
1415
1416 if (isset($moduleFiles[$server])) {
1417 return $moduleFiles[$server]['modul'] ?? '';
1418 }
1419
1420 $config = dbx()->get_config('dbx', 'db');
1421 if (is_array($config) && isset($config[$server])) {
1422 $db = $config[$server];
1423 $type = strtolower((string)($db['type'] ?? ''));
1424 $host = (string)($db['host'] ?? '');
1425 $name = (string)($db['dbname'] ?? ($db['name'] ?? ''));
1426 $isSqlite = ($type == 'sqlite' || $type == 'sqlite3' || preg_match('/\.(db3|sqlite|sqlite3)$/i', $name));
1427
1428 if ($isSqlite && ($host !== '' || $name !== '')) {
1429 $configFile = realpath(dbx()->os_path($host . $name));
1430
1431 if ($configFile) {
1432 $configFile = strtolower(str_replace('\\', '/', $configFile));
1433
1434 foreach ($moduleFiles as $moduleDb) {
1435 $moduleFile = realpath((string)($moduleDb['file'] ?? ''));
1436
1437 if ($moduleFile && strtolower(str_replace('\\', '/', $moduleFile)) === $configFile) {
1438 return $moduleDb['modul'] ?? '';
1439 }
1440 }
1441 }
1442 }
1443
1444 return 'sys';
1445 }
1446
1447 return '';
1448 }
1449
1456
1457 private function field_type_label($field) {
1458 $type = trim((string)($field['type'] ?? ''));
1459 $len = trim((string)($field['length'] ?? ''));
1460 $idx = trim((string)($field['index'] ?? ''));
1461
1462 $label = $type;
1463 if ($len !== '') {
1464 $label .= '(' . $len . ')';
1465 }
1466 if ($idx !== '') {
1467 $label .= ' / ' . $idx;
1468 }
1469
1470 return $label;
1471 }
1472
1480 private function field_tooltip_count($fields, $title = '') {
1481 $texts = $this->schema_texts();
1482 if ($title === '') {
1483 $title = $texts->get_fd_message('fields');
1484 }
1485 $fields = array_values((array)$fields);
1486 $count = count($fields);
1487
1488 $plain = $title . ': ' . $count;
1489 $html = '<div class="dbx-tooltip-fields">';
1490 $html .= '<div class="dbx-tooltip-title">' . $this->esc($title) . '</div>';
1491
1492 if ($count <= 0) {
1493 $plain .= "\n" . $texts->get_fd_message('no_fields');
1494 $html .= '<div class="dbx-tooltip-empty">' . $this->esc($texts->get_fd_message('no_fields')) . '</div>';
1495 } else {
1496 $html .= '<table class="dbx-tooltip-table">';
1497 foreach ($fields as $field) {
1498 $name = trim((string)($field['name'] ?? ''));
1499 if ($name === '') {
1500 continue;
1501 }
1502
1503 $type = $this->field_type_label($field);
1504 $plain .= "\n" . $name . ': ' . $type;
1505
1506 $html .= '<tr>';
1507 $html .= '<td><code>' . $this->esc($name) . '</code></td>';
1508 $html .= '<td>' . $this->esc($type) . '</td>';
1509 $html .= '</tr>';
1510 }
1511 $html .= '</table>';
1512 }
1513
1514 $html .= '</div>';
1515
1516 return '<span class="dbx-schema-field-count badge bg-light text-dark border" '
1517 . 'title="' . $this->esc($plain) . '" '
1518 . 'data-dbx-tooltip="' . $this->esc(str_replace('"', "'", $html)) . '">'
1519 . $this->esc($count)
1520 . '</span>';
1521 }
1522
1529
1530 private function mapping_status_label($status) {
1531 $texts = $this->schema_texts();
1532 $map = array(
1533 'exact' => array(
1534 $texts->get_fd_message('mapping_status_direct'),
1535 'success',
1536 ),
1537 'mapped' => array(
1538 $texts->get_fd_message('mapping_status_mapped'),
1539 'primary',
1540 ),
1541 'type_conflict' => array(
1542 $texts->get_fd_message('mapping_status_type_check'),
1543 'warning text-dark',
1544 ),
1545 'new' => array(
1546 $texts->get_fd_message('mapping_status_new'),
1547 'secondary',
1548 ),
1549 );
1550
1551 $item = $map[$status] ?? array($status, 'secondary');
1552 return '<span class="badge bg-' . $this->esc($item[1]) . '">' . $this->esc($item[0]) . '</span>';
1553 }
1554
1561 private function mapping_kind_label($kind) {
1562 $map = array(
1563 'dd_to_db' => 'DD -> DB',
1564 'db_to_dd' => 'DB -> DD',
1565 'transfer' => 'Transfer',
1566 );
1567
1568 return $map[$kind] ?? 'Schema-Mapping';
1569 }
1570
1577 private function posted_mapping_from_model($model) {
1578 $posted = $_POST['schema_map'] ?? array();
1579 if (!is_array($posted)) {
1580 return array();
1581 }
1582
1583 $mapping = array();
1584 foreach ($posted as $target => $source) {
1585 $target = trim((string)$target);
1586 $source = trim((string)$source);
1587
1588 if ($target === '' || $source === '') {
1589 continue;
1590 }
1591
1592 $mapping[$source] = $target;
1593 }
1594
1595 $oDD = dbx()->get_system_obj('dbxDD');
1596 return $oDD->normalize_schema_mapping(
1597 $mapping,
1598 $model['source_fields'] ?? array(),
1599 $model['target_fields'] ?? array()
1600 );
1601 }
1602
1610 private function source_options_html($sources, $selected) {
1611 $html = '<option value=""></option>';
1612
1613 foreach ($sources as $source) {
1614 $name = (string)($source['name'] ?? '');
1615 if ($name === '') {
1616 continue;
1617 }
1618
1619 $sel = (strcasecmp($name, (string)$selected) === 0) ? ' selected' : '';
1620 $meta = $this->field_type_label($source);
1621 $html .= '<option value="' . $this->esc($name) . '"' . $sel . '>'
1622 . $this->esc($name . ($meta ? ' - ' . $meta : ''))
1623 . '</option>';
1624 }
1625
1626 return $html;
1627 }
1628
1636 private function render_source_items($sources, $mapping) {
1637 $texts = $this->schema_texts();
1638 $mapped = array();
1639 foreach ($mapping as $source => $target) {
1640 $mapped[strtolower((string)$source)] = $target;
1641 }
1642
1643 $html = '';
1644 foreach ($sources as $source) {
1645 $name = (string)($source['name'] ?? '');
1646 if ($name === '') {
1647 continue;
1648 }
1649
1650 $target = $mapped[strtolower($name)] ?? '';
1651 $classes = 'dbx-mapping-source';
1652 if ($target) {
1653 $classes .= ' is-used';
1654 }
1655
1656 $html .= '<button type="button" class="' . $classes . '" draggable="true" '
1657 . 'data-mapping-source="' . $this->esc($name) . '" '
1658 . 'title="' . $this->esc($name) . '">'
1659 . '<span class="dbx-mapping-field-name">' . $this->esc($name) . '</span>'
1660 . '<span class="dbx-mapping-field-meta">' . $this->esc($this->field_type_label($source)) . '</span>'
1661 . '</button>';
1662 }
1663
1664 if ($html === '') {
1665 $html = '<div class="dbx-mapping-empty">'
1666 . $this->esc($texts->get_fd_message('mapping_no_source_fields'))
1667 . '</div>';
1668 }
1669
1670 return $html;
1671 }
1672
1679 private function render_mapping_rows($model) {
1680 $texts = $this->schema_texts();
1681 $sources = $model['source_fields'] ?? array();
1682 $html = '';
1683
1684 foreach (($model['target_rows'] ?? array()) as $row) {
1685 $target = $row['target'] ?? array();
1686 $targetName = (string)($row['target_name'] ?? ($target['name'] ?? ''));
1687 if ($targetName === '') {
1688 continue;
1689 }
1690
1691 $sourceName = (string)($row['source_name'] ?? '');
1692 $status = (string)($row['status'] ?? 'new');
1693
1694 $html .= '<div class="dbx-mapping-target" data-mapping-target="' . $this->esc($targetName) . '" '
1695 . 'data-mapping-status="' . $this->esc($status) . '">';
1696 $html .= '<div class="dbx-mapping-target-main">';
1697 $html .= '<div class="dbx-mapping-target-title">';
1698 $html .= '<span class="dbx-mapping-field-name">' . $this->esc($targetName) . '</span>';
1699 $html .= $this->mapping_status_label($status);
1700 $html .= '</div>';
1701 $html .= '<span class="dbx-mapping-field-meta">' . $this->esc($this->field_type_label($target)) . '</span>';
1702 $html .= '</div>';
1703
1704 $html .= '<div class="dbx-mapping-drop" data-mapping-drop="' . $this->esc($targetName) . '">';
1705 $html .= '<select class="form-select form-select-sm" name="schema_map[' . $this->esc($targetName) . ']" '
1706 . 'data-mapping-select data-target="' . $this->esc($targetName) . '" '
1707 . 'data-auto-source="' . $this->esc($sourceName) . '">';
1708 $html .= $this->source_options_html($sources, $sourceName);
1709 $html .= '</select>';
1710 $html .= '<button type="button" class="btn btn-sm btn-outline-secondary" data-mapping-clear-row title="'
1711 . $this->esc($texts->get_fd_message('mapping_clear_assignment')) . '">'
1712 . '<i class="bi bi-x-lg"></i></button>';
1713 $html .= '</div>';
1714 $html .= '</div>';
1715 }
1716
1717 if ($html === '') {
1718 $html = '<div class="dbx-mapping-empty">'
1719 . $this->esc($texts->get_fd_message('mapping_no_target_fields'))
1720 . '</div>';
1721 }
1722
1723 return $html;
1724 }
1725
1732 private function render_mapping_board($model) {
1733 $texts = $this->schema_texts();
1734 $kind = (string)($model['kind'] ?? 'dd_to_db');
1735 $mapping = $model['mapping'] ?? array();
1736 $sourceCount = count($model['source_fields'] ?? array());
1737 $targetCount = count($model['target_fields'] ?? array());
1738 $mappedCount = count($mapping);
1739 $file = (string)($model['file'] ?? '');
1740 $fileLabel = $file !== '' ? $this->path_rel($file) : '';
1741 $updated = (string)($model['updated_at'] ?? '');
1742
1743 $html = '<div class="dbx-schema-mapping" data-dbx="lib=mapping" data-mapping-root="1">';
1744
1745 $html .= '<div class="dbx-mapping-head">';
1746 $html .= '<div>';
1747 $html .= '<h3>' . $this->esc($this->mapping_kind_label($kind)) . '</h3>';
1748 $html .= '<div class="dbx-mapping-subtitle">'
1749 . '<span>' . $this->esc($model['source_label'] ?? '') . '</span>'
1750 . '<i class="bi bi-arrow-right"></i>'
1751 . '<span>' . $this->esc($model['target_label'] ?? '') . '</span>'
1752 . '</div>';
1753 $html .= '</div>';
1754
1755 $html .= '<div class="dbx-mapping-tools">';
1756 $html .= '<span class="badge bg-secondary" data-mapping-count="mapped">' . $this->esc($mappedCount . ' / ' . $targetCount) . '</span>';
1757 $html .= '<button type="button" class="btn btn-outline-primary" data-mapping-action="auto" title="'
1758 . $this->esc($texts->get_fd_message('mapping_auto'))
1759 . '"><i class="bi bi-magic"></i></button>';
1760 $html .= '<button type="button" class="btn btn-outline-secondary" data-mapping-action="clear" title="'
1761 . $this->esc($texts->get_fd_message('mapping_clear'))
1762 . '"><i class="bi bi-eraser"></i></button>';
1763 $html .= '</div>';
1764 $html .= '</div>';
1765
1766 $html .= '<div class="dbx-mapping-meta">';
1767 $html .= '<span>' . $this->esc($texts->get_fd_message('mapping_source'))
1768 . ': ' . $this->esc((string)$sourceCount) . '</span>';
1769 $html .= '<span>' . $this->esc($texts->get_fd_message('mapping_target'))
1770 . ': ' . $this->esc((string)$targetCount) . '</span>';
1771 if ($updated !== '') {
1772 $html .= '<span>' . $this->esc($texts->get_fd_message('mapping_saved_at'))
1773 . ': ' . $this->esc($updated) . '</span>';
1774 }
1775 if ($fileLabel !== '') {
1776 $html .= '<span class="dbx-mapping-file">' . $this->esc($fileLabel) . '</span>';
1777 }
1778 $html .= '</div>';
1779
1780 $html .= '<div class="dbx-mapping-workbench">';
1781 $html .= '<section class="dbx-mapping-panel dbx-mapping-panel-source">';
1782 $html .= '<header><span>'
1783 . $this->esc($texts->get_fd_message('mapping_source'))
1784 . '</span><span class="badge bg-light text-dark">'
1785 . $this->esc((string)$sourceCount) . '</span></header>';
1786 $html .= '<div class="dbx-mapping-source-list">' . $this->render_source_items($model['source_fields'] ?? array(), $mapping) . '</div>';
1787 $html .= '</section>';
1788
1789 $html .= '<div class="dbx-mapping-canvas" aria-hidden="true"><svg data-mapping-lines></svg></div>';
1790
1791 $html .= '<section class="dbx-mapping-panel dbx-mapping-panel-target">';
1792 $html .= '<header><span>'
1793 . $this->esc($texts->get_fd_message('mapping_target'))
1794 . '</span><span class="badge bg-light text-dark">'
1795 . $this->esc((string)$targetCount) . '</span></header>';
1796 $html .= '<div class="dbx-mapping-target-list">' . $this->render_mapping_rows($model) . '</div>';
1797 $html .= '</section>';
1798 $html .= '</div>';
1799
1800 $html .= '</div>';
1801
1802 return $html;
1803 }
1804
1811 private function sync_label_from_plan($plan) {
1812 $texts = $this->schema_texts();
1813 if (!is_array($plan) || empty($plan['ok'])) {
1814 return $this->badge($texts->get_fd_message('status_dd_error'), 'danger');
1815 }
1816
1817 if (empty($plan['table_exists'])) {
1818 return $this->badge($texts->get_fd_message('status_db_missing'), 'warning');
1819 }
1820
1821 $adds = count($plan['add_fields'] ?? array());
1822 $idx = count($plan['add_indexes'] ?? array());
1823 $miss = count($plan['missing_in_dd'] ?? array());
1824 $conf = count($plan['type_conflicts'] ?? array());
1825
1826 if (!empty($plan['rebuild_needed'])) {
1827 return $this->badge($texts->get_fd_message('status_rebuild'), 'danger');
1828 }
1829
1830 if ($adds || $idx) {
1831 return $this->badge($texts->get_fd_message('status_sync_open'), 'warning');
1832 }
1833
1834 return $this->badge($texts->get_fd_message('status_synced'), 'success');
1835 }
1836
1843 private function sync_detail_from_plan($plan) {
1844 if (!is_array($plan) || empty($plan['ok']) || empty($plan['table_exists'])) {
1845 return '';
1846 }
1847
1848 $adds = count($plan['add_fields'] ?? array());
1849 $idx = count($plan['add_indexes'] ?? array());
1850 $miss = count($plan['missing_in_dd'] ?? array());
1851 $conf = count($plan['type_conflicts'] ?? array());
1852
1853 if (!empty($plan['rebuild_needed'])) {
1854 return $this->esc(
1855 $this->schema_texts()->format_fd_message('detail_type', array('count' => $conf))
1856 . ' / '
1857 . $this->schema_texts()->format_fd_message('detail_extra', array('count' => $miss))
1858 );
1859 }
1860
1861 if ($adds || $idx) {
1862 return $this->esc(
1863 $this->schema_texts()->format_fd_message('detail_field', array('count' => $adds))
1864 . ' / '
1865 . $this->schema_texts()->format_fd_message('detail_index', array('count' => $idx))
1866 );
1867 }
1868
1869 return '';
1870 }
1871
1879 private function get_sync_plan($modul, $dd) {
1880 $oDD = dbx()->get_system_obj('dbxDD');
1881 return $oDD->sync_dd_to_db($modul, $dd, 'plan');
1882 }
1883
1889 private function get_dd_records() {
1890 $records = array();
1891 $oDD = dbx()->get_system_obj('dbxDD');
1892 $base = str_replace('\\', '/', dbx()->get_base_dir()) . 'dbx/modules/*/dd/*.dd.php';
1893
1894 foreach (glob($base) as $file) {
1895 $norm = str_replace('\\', '/', $file);
1896
1897 if (!preg_match('#/dbx/modules/([^/]+)/dd/([^/]+)\.dd\.php$#', $norm, $match)) {
1898 continue;
1899 }
1900
1901 $modul = $match[1];
1902 $dd = $match[2];
1903 if ($dd === 'new') {
1904 continue;
1905 }
1906
1907 $ddRef = $this->dd_ref($modul, $dd);
1908 $model = $oDD->get_dd_model($ddRef);
1909 if (!$model) {
1910 continue;
1911 }
1912
1913 $server = $model['table']['server'] ?? '';
1914 $table = $model['table']['table'] ?? '';
1915 if (str_starts_with(strtolower((string)$table), 'sqlite_')) {
1916 continue;
1917 }
1918 $exists = ($server && $table) ? $oDD->get_table_exist($server, $table) : 0;
1919 $count = $exists ? $oDD->count($ddRef) : '-';
1920 $plan = $this->get_sync_plan($modul, $dd);
1921 $actions = $this->dd_actions($modul, $dd, $server, $table, $plan);
1922
1923 $ddFields = array_values((array)($model['fields'] ?? array()));
1924 $dbFields = ($server && $table && $exists) ? $oDD->get_db_fields($server, $table) : array();
1925
1926 $records[] = array(
1927 'rid' => $modul . '|' . $dd,
1928 'modul' => $modul,
1929 'dd' => $dd,
1930 'path' => $this->path_rel($norm),
1931 'server' => $server,
1932 'database' => $this->get_database_label($server),
1933 'table' => $table,
1934 'dd_fields' => $this->field_tooltip_count($ddFields, $this->schema_texts()->get_fd_message('dd_fields')),
1935 'db_fields' => $this->field_tooltip_count($dbFields, $this->schema_texts()->get_fd_message('db_fields')),
1936 'count' => $count,
1937 'sync' => $this->sync_label_from_plan($plan),
1938 'sync_info' => $this->sync_detail_from_plan($plan),
1939 'act_sync' => $actions['sync'] ?? '',
1940 'act_map' => $actions['map'] ?? '',
1941 'act_move' => $actions['transfer'] ?? '',
1942 'act_flds' => $actions['fields'] ?? '',
1943 );
1944 }
1945
1946 usort($records, function($a, $b) {
1947 return strcmp($a['modul'] . '|' . $a['dd'], $b['modul'] . '|' . $b['dd']);
1948 });
1949
1950 return $records;
1951 }
1952
1959 private function db_server_aliases($server) {
1960 $aliases = array();
1961 $server = trim((string)$server);
1962
1963 if ($server === '') {
1964 return array();
1965 }
1966
1967 $add = function($value) use (&$aliases) {
1968 $value = trim(str_replace('\\', '/', (string)$value));
1969 if ($value !== '') {
1970 $aliases[$value] = true;
1971 }
1972 };
1973
1974 $add($server);
1975
1976 $norm = str_replace('\\', '/', $server);
1977 if ($norm !== $server) {
1978 $add($norm);
1979 }
1980
1981 if (strpos($norm, '/') !== false) {
1982 $add(basename($norm));
1983 }
1984
1985 if (strpos($server, '|') !== false) {
1986 $parts = explode('|', $server, 2);
1987 $modul = trim((string)($parts[0] ?? ''));
1988 $name = trim((string)($parts[1] ?? ''));
1989
1990 if ($name !== '') {
1991 $add($name);
1992 }
1993
1994 if ($modul !== '' && $name !== '') {
1995 $add($modul . '|' . $name);
1996 $add($modul . '/db/' . $name);
1997 }
1998 }
1999
2000 $moduleFile = $this->resolve_module_db_file($server);
2001 if ($moduleFile) {
2002 $file = str_replace('\\', '/', $moduleFile);
2003 $add($file);
2004 $add($this->path_rel($file));
2005 $add(basename($file));
2006
2007 if (preg_match('#/dbx/modules/([^/]+)/db/([^/]+)$#', $file, $match)) {
2008 $add($match[2]);
2009 $add($match[1] . '|' . $match[2]);
2010 $add($match[1] . '/db/' . $match[2]);
2011 }
2012 }
2013
2014 return array_keys($aliases);
2015 }
2016
2023
2024
2025 private function get_dd_index_by_db($ddRecords) {
2026 $index = array();
2027
2028 foreach ($ddRecords as $record) {
2029 $server = $record['server'] ?? '';
2030 $table = $record['table'] ?? '';
2031
2032 if (!$server || !$table) {
2033 continue;
2034 }
2035
2036 $rid = (string)($record['rid'] ?? '');
2037 if ($rid === '') {
2038 $rid = (string)($record['modul'] ?? '') . '|' . (string)($record['dd'] ?? '');
2039 }
2040
2041 foreach ($this->db_server_aliases($server) as $alias) {
2042 $key = $this->norm_key($alias, $table);
2043 if (!isset($index[$key])) {
2044 $index[$key] = array();
2045 }
2046
2047 $index[$key][$rid] = $record;
2048 }
2049 }
2050
2051 foreach ($index as $key => $records) {
2052 $index[$key] = array_values($records);
2053 }
2054
2055 return $index;
2056 }
2057
2066 private function get_dd_records_for_db($ddIndex, $server, $table) {
2067 $records = array();
2068
2069 if (!$server || !$table) {
2070 return array();
2071 }
2072
2073 foreach ($this->db_server_aliases($server) as $alias) {
2074 $key = $this->norm_key($alias, $table);
2075 if (!isset($ddIndex[$key])) {
2076 continue;
2077 }
2078
2079 foreach ($ddIndex[$key] as $record) {
2080 $rid = (string)($record['rid'] ?? '');
2081 if ($rid === '') {
2082 $rid = (string)($record['modul'] ?? '') . '|' . (string)($record['dd'] ?? '');
2083 }
2084
2085 $records[$rid] = $record;
2086 }
2087 }
2088
2089 return array_values($records);
2090 }
2091
2102
2103
2104 private function dd_actions($modul, $dd, $server, $table, $plan) {
2105 $actions = array(
2106 'sync' => '',
2107 'map' => '',
2108 'transfer' => '',
2109 'fields' => '',
2110 );
2111 $mode = (!empty($plan['rebuild_needed'])) ? 'force' : 'apply';
2112
2113 $syncUrl = $this->build_url('dd', 'sync_dd_to_db', array(
2114 'modul' => $modul,
2115 'dd' => $dd,
2116 'mode' => $mode,
2117 'reset' => 1,
2118 ));
2119 $actions['sync'] = $this->openwin($syncUrl, 'bi bi-arrow-repeat', $this->schema_texts()->get_fd_message('action_dd_to_db'), 1000, 700);
2120
2121 $mapUrl = $this->build_url('dd', 'mapping', array(
2122 'kind' => 'dd_to_db',
2123 'modul' => $modul,
2124 'dd' => $dd,
2125 ));
2126 $actions['map'] = $this->openwin($mapUrl, 'bi bi-diagram-3', $this->schema_texts()->get_fd_message('action_mapping'), 1280, 880);
2127
2128 if ($server && $table) {
2129 $transferUrl = $this->build_url('dd', 'transfer', array(
2130 'source_server' => $server,
2131 'source_table' => $table,
2132 ));
2133 $actions['transfer'] = $this->openwin($transferUrl, 'bi bi-box-arrow-right', $this->schema_texts()->get_fd_message('action_transfer'), 980, 720);
2134 }
2135
2136 $fieldsUrl = $this->build_url('dd', 'fields', array(
2137 'modul' => $modul,
2138 'dd' => $dd,
2139 ));
2140 $actions['fields'] = $this->openwin($fieldsUrl, 'bi bi-list-columns', $this->schema_texts()->get_fd_message('action_dd_fields'), 1400, 860);
2141
2142 return $actions;
2143 }
2144
2153 private function db_actions($server, $table, $dds) {
2154 $actions = array(
2155 'sync' => '',
2156 'sync_dd_to_db' => '',
2157 'map' => '',
2158 'transfer' => '',
2159 );
2160
2161 if (is_array($dds) && count($dds)) {
2162 $first = $dds[0];
2163 $plan = $this->get_sync_plan($first['modul'], $first['dd']);
2164 $ddToDbMode = (!empty($plan['rebuild_needed'])) ? 'force' : 'apply';
2165
2166 $syncUrl = $this->build_url('db', 'sync_db_to_dd', array(
2167 'server' => $server,
2168 'table' => $table,
2169 'modul' => $first['modul'],
2170 'dd' => $first['dd'],
2171 'mode' => 'merge',
2172 'reset' => 1,
2173 ));
2174 $actions['sync'] = $this->openwin($syncUrl, 'bi bi-arrow-down-up', $this->schema_texts()->get_fd_message('action_db_to_dd'), 1000, 700);
2175
2176 $ddToDbUrl = $this->build_url('db', 'sync_dd_to_db', array(
2177 'modul' => $first['modul'],
2178 'dd' => $first['dd'],
2179 'mode' => $ddToDbMode,
2180 'reset' => 1,
2181 ));
2182 $actions['sync_dd_to_db'] = $this->openwin($ddToDbUrl, 'bi bi-arrow-repeat', $this->schema_texts()->get_fd_message('action_db_from_dd'), 1000, 700);
2183
2184 $mapUrl = $this->build_url('db', 'mapping', array(
2185 'kind' => 'db_to_dd',
2186 'server' => $server,
2187 'table' => $table,
2188 'modul' => $first['modul'],
2189 'dd' => $first['dd'],
2190 ));
2191 $actions['map'] = $this->openwin($mapUrl, 'bi bi-diagram-3', $this->schema_texts()->get_fd_message('action_mapping'), 1280, 880);
2192 } else {
2193 $syncUrl = $this->build_url('db', 'sync_db_to_dd', array(
2194 'server' => $server,
2195 'table' => $table,
2196 ));
2197 $actions['sync'] = $this->openwin($syncUrl, 'bi bi-file-earmark-plus', $this->schema_texts()->get_fd_message('action_create_dd'), 980, 720);
2198 }
2199
2200 $transferUrl = $this->build_url('db', 'transfer', array(
2201 'source_server' => $server,
2202 'source_table' => $table,
2203 ));
2204 $actions['transfer'] = $this->openwin($transferUrl, 'bi bi-box-arrow-right', $this->schema_texts()->get_fd_message('action_transfer'), 980, 720);
2205
2206 return $actions;
2207 }
2208
2215 private function get_db_records($ddRecords) {
2216 $records = array();
2217 $oDB = dbx()->get_system_obj('dbxDB');
2218 $oDD = dbx()->get_system_obj('dbxDD');
2219 $ddIndex = $this->get_dd_index_by_db($ddRecords);
2220 $servers = $this->get_server_options();
2221
2222 foreach ($servers as $server => $label) {
2223 $tables = $oDB->get_db_tables($server);
2224 if (!is_array($tables)) {
2225 $tables = array();
2226 }
2227
2228 $hasTables = false;
2229
2230 foreach ($tables as $tableRec) {
2231 $table = $tableRec['name'] ?? '';
2232 if (!$table) {
2233 continue;
2234 }
2235
2236 $hasTables = true;
2237
2238 $dds = $this->get_dd_records_for_db($ddIndex, $server, $table);
2239 $ddLabels = array();
2240 $sync = $this->badge($this->schema_texts()->get_fd_message('no_dd'), 'secondary');
2241 $syncInfo = '';
2242
2243 foreach ($dds as $ddRec) {
2244 $ddLabels[] = $this->esc($ddRec['modul'] . '|' . $ddRec['dd']);
2245 }
2246
2247 if ($dds) {
2248 $sync = $dds[0]['sync'];
2249 $syncInfo = $dds[0]['sync_info'] ?? '';
2250 }
2251
2252 $dbFields = $oDD->get_db_fields($server, $table);
2253 $ddFields = array();
2254
2255 if ($dds) {
2256 $first = $dds[0];
2257 $model = $oDD->get_dd_model($this->dd_ref($first['modul'], $first['dd']));
2258 $ddFields = array_values((array)($model['fields'] ?? array()));
2259 }
2260
2261 $actions = $this->db_actions($server, $table, $dds);
2262
2263 $records[] = array(
2264 'rid' => $this->encode_db_rid($server, $table),
2265 'modul' => $this->get_server_module($server),
2266 'server' => $server,
2267 'database' => $this->get_database_label($server),
2268 'path' => $this->get_database_path_label($server),
2269 'table' => $table,
2270 'db_fields' => $this->field_tooltip_count($dbFields, $this->schema_texts()->get_fd_message('db_fields')),
2271 'dd_fields' => $this->field_tooltip_count($ddFields, $this->schema_texts()->get_fd_message('dd_fields')),
2272 'count' => $tableRec['count'] ?? '-',
2273 'dd' => implode('<br>', $ddLabels),
2274 'sync' => $sync,
2275 'sync_info' => $syncInfo,
2276 'act_sync' => $actions['sync'] ?? '',
2277 'act_dd_sync' => $actions['sync_dd_to_db'] ?? '',
2278 'act_map' => $actions['map'] ?? '',
2279 'act_move' => $actions['transfer'] ?? '',
2280 'dd_edit_modul' => $dds[0]['modul'] ?? '',
2281 'dd_edit_dd' => $dds[0]['dd'] ?? '',
2282 );
2283 }
2284
2285 if (!$hasTables) {
2286 $records[] = array(
2287 'rid' => $this->encode_db_rid($server, ''),
2288 'modul' => $this->get_server_module($server),
2289 'server' => $server,
2290 'database' => $this->get_database_label($server),
2291 'path' => $this->get_database_path_label($server),
2292 'table' => $this->badge($this->schema_texts()->get_fd_message('no_tables'), 'secondary'),
2293 'db_fields' => $this->field_tooltip_count(array(), $this->schema_texts()->get_fd_message('db_fields')),
2294 'dd_fields' => $this->field_tooltip_count(array(), $this->schema_texts()->get_fd_message('dd_fields')),
2295 'count' => '-',
2296 'dd' => '',
2297 'sync' => $this->badge($this->schema_texts()->get_fd_message('no_tables'), 'secondary'),
2298 'sync_info' => '',
2299 'act_sync' => '',
2300 'act_dd_sync' => '',
2301 'act_map' => '',
2302 'act_move' => '',
2303 'dd_edit_modul' => '',
2304 'dd_edit_dd' => '',
2305 );
2306 }
2307 }
2308
2309 usort($records, function($a, $b) {
2310 return strcmp(strip_tags($a['modul'] . '|' . $a['server'] . '|' . $a['table']), strip_tags($b['modul'] . '|' . $b['server'] . '|' . $b['table']));
2311 });
2312
2313 return $records;
2314 }
2315
2323 private function backup_table($server, $table) {
2324 $texts = $this->schema_texts();
2325 $oDB = dbx()->get_system_obj('dbxDB');
2326 $oDD = dbx()->get_system_obj('dbxDD');
2327
2328 if (!$server || !$table || !$oDD->get_table_exist($server, $table)) {
2329 return array('ok' => 0, 'msg' => $texts->get_fd_message('backup_table_not_found'));
2330 }
2331
2332 $fields = $oDD->get_db_fields($server, $table);
2333 if (!$fields) {
2334 return array('ok' => 0, 'msg' => $texts->get_fd_message('backup_fields_read_error'));
2335 }
2336
2337 $indexes = $oDD->get_db_indexes($server, $table);
2338 $rows = $oDB->rawQuery($server, 'SELECT * FROM ' . $this->quote_db_ident($server, $table));
2339 if (!is_array($rows)) {
2340 return array('ok' => 0, 'msg' => $texts->get_fd_message('backup_data_read_error'));
2341 }
2342
2343 $created = date('Ymd-His');
2344 $file = $created . '__' . $this->backup_name_part($server) . '__' . $this->backup_name_part($table) . '.json';
2345 $path = $this->backup_dir() . $file;
2346 $tmp = $path . '.tmp';
2347
2348 $payload = array(
2349 'version' => 1,
2350 'created_at' => date('Y-m-d H:i:s'),
2351 'server' => $server,
2352 'table' => $table,
2353 'db_type' => $oDB->get_db_type($server),
2354 'count' => count($rows),
2355 'fields' => $fields,
2356 'indexes' => $indexes,
2357 'rows' => $rows,
2358 );
2359
2360 $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
2361 if ($json === false || file_put_contents($tmp, $json) === false || !@rename($tmp, $path)) {
2362 @unlink($tmp);
2363 return array('ok' => 0, 'msg' => $texts->get_fd_message('backup_write_error'));
2364 }
2365
2366 return array('ok' => 1, 'file' => $file, 'path' => $path, 'count' => count($rows));
2367 }
2368
2375 private function backup_meta($file) {
2376 $path = $this->backup_file_path($file);
2377 if (!$path) {
2378 return array();
2379 }
2380
2381 $raw = file_get_contents($path);
2382 $data = $raw !== false ? json_decode($raw, true) : null;
2383 if (!is_array($data)) {
2384 return array();
2385 }
2386
2387 $data['_file'] = basename($path);
2388 $data['_path'] = $path;
2389 return $data;
2390 }
2391
2399 private function table_backups($server, $table) {
2400 $items = array();
2401 foreach (glob($this->backup_dir() . '*.json') as $file) {
2402 $meta = $this->backup_meta(basename($file));
2403 if (($meta['server'] ?? '') === $server && ($meta['table'] ?? '') === $table) {
2404 $items[] = $meta;
2405 }
2406 }
2407
2408 usort($items, function($a, $b) {
2409 return strcmp((string)($b['created_at'] ?? ''), (string)($a['created_at'] ?? ''));
2410 });
2411
2412 return $items;
2413 }
2414
2422 private function latest_backup($server, $table) {
2423 $items = $this->table_backups($server, $table);
2424 return $items[0] ?? array();
2425 }
2426
2434 private function backup_label($server, $table) {
2435 $texts = $this->schema_texts();
2436 $meta = $this->latest_backup($server, $table);
2437 if (!$meta) {
2438 return $this->badge($texts->get_fd_message('no_backup'), 'secondary');
2439 }
2440
2441 $created = (string)($meta['created_at'] ?? '');
2442 $count = (int)($meta['count'] ?? 0);
2443 return $this->esc(
2444 $created . ' / ' . $texts->format_fd_message('record_count_short', array('count' => $count))
2445 );
2446 }
2447
2454 private function restore_table_from_backup($file) {
2455 $texts = $this->schema_texts();
2456 $meta = $this->backup_meta($file);
2457 if (!$meta) {
2458 return array('ok' => 0, 'msg' => $texts->get_fd_message('backup_invalid_file'));
2459 }
2460
2461 $server = (string)($meta['server'] ?? '');
2462 $table = (string)($meta['table'] ?? '');
2463 $fields = is_array($meta['fields'] ?? null) ? $meta['fields'] : array();
2464 $indexes = is_array($meta['indexes'] ?? null) ? $meta['indexes'] : array();
2465 $rows = is_array($meta['rows'] ?? null) ? $meta['rows'] : array();
2466
2467 if (!$server || !$table || !$fields) {
2468 return array('ok' => 0, 'msg' => $texts->get_fd_message('backup_incomplete_metadata'));
2469 }
2470
2471 $oDB = dbx()->get_system_obj('dbxDB');
2472 $oDD = dbx()->get_system_obj('dbxDD');
2473
2474 $oDD->drop_db_tab($server, $table);
2475 if (!$oDD->create_db_tab_from_fields($server, $table, $fields, $indexes)) {
2476 return array('ok' => 0, 'msg' => $texts->get_fd_message('restore_create_table_error'));
2477 }
2478
2479 if (!$rows) {
2480 return array('ok' => 1, 'count' => 0, 'file' => basename((string)$file));
2481 }
2482
2483 $names = array();
2484 foreach ($fields as $field) {
2485 $name = (string)($field['name'] ?? '');
2486 if ($name !== '') {
2487 $names[] = $name;
2488 }
2489 }
2490
2491 if (!$names) {
2492 return array('ok' => 0, 'msg' => $texts->get_fd_message('restore_no_target_fields'));
2493 }
2494
2495 $qNames = array_map(function($name) use ($server) {
2496 return $this->quote_db_ident($server, $name);
2497 }, $names);
2498
2499 foreach ($rows as $row) {
2500 $values = array();
2501 foreach ($names as $name) {
2502 $values[] = $this->sql_db_value($server, is_array($row) && array_key_exists($name, $row) ? $row[$name] : null);
2503 }
2504
2505 $sql = 'INSERT INTO ' . $this->quote_db_ident($server, $table)
2506 . ' (' . implode(',', $qNames) . ') VALUES (' . implode(',', $values) . ')';
2507 $ok = $oDB->rawQuery($server, $sql);
2508 if (!is_array($ok) && (int)$ok === 0 && $oDB->get_error_status() !== '') {
2509 return array('ok' => 0, 'msg' => 'Daten konnten nicht eingetragen werden.');
2510 }
2511 }
2512
2513 return array('ok' => 1, 'count' => count($rows), 'file' => basename((string)$file));
2514 }
2515
2523 private function filter_rows($rows, $search) {
2524 $search = trim((string) $search);
2525 if ($search === '') {
2526 return $rows;
2527 }
2528
2529 $filtered = array();
2530 foreach ($rows as $row) {
2531 $haystack = strtolower(strip_tags(implode(' ', $row)));
2532 if (strpos($haystack, strtolower($search)) !== false) {
2533 $filtered[] = $row;
2534 }
2535 }
2536
2537 return $filtered;
2538 }
2539
2545 private function has_config_db_servers() {
2546 $config = dbx()->get_config('dbx', 'db');
2547 if (!is_array($config) || count($config) <= 0) {
2548 return false;
2549 }
2550
2551 $moduleFiles = $this->get_module_db_files();
2552 $moduleFileIndex = array();
2553
2554 foreach ($moduleFiles as $server => $db) {
2555 $real = realpath((string)($db['file'] ?? ''));
2556 if ($real) {
2557 $moduleFileIndex[strtolower(str_replace('\\', '/', $real))] = 1;
2558 }
2559 }
2560
2561 foreach ($config as $server => $data) {
2562 if (isset($moduleFiles[$server])) {
2563 continue;
2564 }
2565
2566 $type = strtolower((string)($data['type'] ?? ''));
2567 $host = (string)($data['host'] ?? '');
2568 $name = (string)($data['dbname'] ?? ($data['name'] ?? ''));
2569 $isSqlite = ($type == 'sqlite' || $type == 'sqlite3' || preg_match('/\.(db3|sqlite|sqlite3)$/i', $name));
2570
2571 if ($isSqlite && ($host !== '' || $name !== '')) {
2572 $file = dbx()->os_path($host . $name);
2573 $real = realpath($file);
2574
2575 if ($real) {
2576 $real = strtolower(str_replace('\\', '/', $real));
2577 if (isset($moduleFileIndex[$real])) {
2578 continue;
2579 }
2580 }
2581 }
2582
2583 return true;
2584 }
2585
2586 return false;
2587 }
2588
2596 private function get_module_filter_options($rows, $includeSys = false) {
2597 $options = array('0' => $this->schema_texts()->get_fd_message('all_modules'));
2598
2599 if ($includeSys) {
2600 $options['sys'] = 'sys';
2601 }
2602
2603 foreach ($rows as $row) {
2604 $modul = trim((string)($row['modul'] ?? ''));
2605 if ($modul !== '') {
2606 $options[$modul] = $modul;
2607 }
2608 }
2609
2610 ksort($options);
2611 return array('0' => $this->schema_texts()->get_fd_message('all_modules')) + $options;
2612 }
2613
2621 private function filter_rows_by_module($rows, $modul) {
2622 $modul = trim((string)$modul);
2623 if ($modul === '' || $modul === '0') {
2624 return $rows;
2625 }
2626
2627 $filtered = array();
2628 foreach ($rows as $row) {
2629 if (strcasecmp((string)($row['modul'] ?? ''), $modul) === 0) {
2630 $filtered[] = $row;
2631 }
2632 }
2633
2634 return $filtered;
2635 }
2636
2648 private function run_schema_report($mode, $rows, $flds, $action, $batchOptions, $message = '') {
2649 $oReport = dbx()->get_system_obj('dbxReport');
2650 $fidMap = array(
2651 'dd' => 'report-dd-sync',
2652 'db' => 'report-db-sync',
2653 'backup' => 'report-db-backup',
2654 'restore' => 'report-db-restore',
2655 );
2656 $fid = $fidMap[$mode] ?? 'report-db-sync';
2657 $moduleOptions = $this->get_module_filter_options($rows, $mode != 'dd' && $this->has_config_db_servers());
2658 $isSchemaList = in_array($mode, array('dd', 'db'), true);
2659
2660 $oReport->init($fid, 'report-schema');
2661 $oReport->_fd = 'dbxAdmin|schema-report';
2662 $oReport->load_fd_messages();
2663 $oReport->set_form_help_enabled(false);
2664 $oReport->set_callback_owner($this);
2665 $oReport->set_callback('row_action_data', 'schema_row_action_data');
2666 $oReport->_action = $action;
2667 $oReport->_rflds = $flds;
2668 $oReport->_mode = 'table';
2669 $oReport->_pages = true;
2670 $oReport->_rrows = 25;
2671 $oReport->_rpos = 0;
2672 $oReport->_but_pagination = 7;
2673 $oReport->_create_row_select = true;
2674 $oReport->_create_row_edit = $isSchemaList;
2675 $oReport->_create_row_show = $isSchemaList;
2676 $oReport->_create_row_delete = $isSchemaList;
2677 $oReport->_create_sel_flds = false;
2678 $oReport->_fld_id = 'rid';
2679 $oReport->_table_buttons = 'left';
2680 $texts = $this->schema_texts();
2681 $oReport->_msg_confirm_delete = ($mode == 'dd')
2682 ? $texts->get_fd_message('confirm_delete_dd')
2683 : $texts->get_fd_message('confirm_delete_db');
2684 $oReport->_msg_info = (string)$message;
2685 $oReport->_tabel_tpls['tpl_row_edit'] = 'modul|schema_row_edit';
2686 $oReport->_tabel_tpls['tpl_row_show'] = 'modul|schema_row_show';
2687 $oReport->_tabel_tpls['tpl_row_delete'] = 'modul|schema_row_delete';
2688
2689 $oReport->add_action('rows_select', 'action_button_select', '&dbx_do=multi_select');
2690 $oReport->add_action('rows_deselect', 'action_button_deselect', '&dbx_do=multi_deselect');
2691
2692 $barPrefix = in_array($mode, array('dd', 'backup', 'restore'), true) ? $mode : 'db';
2693 $oReport->add_rep('bar_title', $texts->get_fd_message('bar_' . $barPrefix . '_title'));
2694 $oReport->add_rep('bar_subtitle', $texts->get_fd_message('bar_' . $barPrefix . '_subtitle'));
2695 $oReport->add_fld('dbx_rmodul', 'select-single-label', label: $texts->get_fd_message('label_module'), rules: 'parameter', options: $moduleOptions);
2696 $oReport->add_fld('dbx_rwhere', 'dbx|search', label: $texts->get_fd_message('label_search'), rules: 'sqlsearch|max=80');
2697 $oReport->add_fld('dbx_rrows', 'integer-label', label: $texts->get_fd_message('label_rows'), rules: 'int');
2698 $oReport->add_fld('maction_select', 'select-single', label: $texts->get_fd_message('label_batch'), rules: 'parameter', options: $batchOptions);
2699 $oReport->add_obj('maction_submit', 'dbx|button-submit', data: 'label=' . $texts->get_fd_message('action_start'));
2700
2701 if ($oReport->submit()) {
2702 $act = $oReport->get_fld_val('maction_select', '0', 'parameter');
2703 if ($act && $act != '0') {
2704 $selects = array_keys($oReport->get_multi_selects());
2705 if ($selects) {
2706 return $this->start_batch($act, $selects);
2707 }
2708
2709 $oReport->_msg_warning = $texts->get_fd_message('no_rows_selected');
2710 }
2711 }
2712
2713 $modul = $oReport->get_fld_val('dbx_rmodul', '0', 'parameter');
2714 $search = $oReport->get_fld_val('dbx_rwhere', '', 'sqlsearch|max=80');
2715 $rrows = $oReport->get_fld_val('dbx_rrows', 25, 'int');
2716 $rpos = $oReport->get_fld_val('dbx_rpos', 0, 'int');
2717 if ($rrows <= 0) {
2718 $rrows = 25;
2719 }
2720
2721 $rows = $this->filter_rows_by_module($rows, $modul);
2722 $rows = $this->filter_rows($rows, $search);
2723 $oReport->_rrows = $rrows;
2724 $oReport->_rpos = $rpos;
2725 $oReport->_rcount = count($rows);
2726 $oReport->_rdata = $oReport->data_rows($rows, $rpos, $rrows);
2727
2728 return $oReport->run();
2729 }
2730
2738 public function schema_row_action_data($report, $data) {
2739 if (!is_array($data) || !isset($data['data']) || !is_array($data['data'])) {
2740 return $data;
2741 }
2742
2743 $type = (string)($data['type'] ?? '');
2744 if (!in_array($type, array('edit', 'show', 'delete'), true)) {
2745 return $data;
2746 }
2747
2748 $record = is_array($data['record'] ?? null) ? $data['record'] : array();
2749 $rid = (string)($data['data']['rid'] ?? ($record['rid'] ?? ''));
2750 $mode = str_starts_with($rid, 'db_') ? 'db' : 'dd';
2751 $base = $this->build_url($mode, $mode == 'dd' ? 'list_dd' : 'list_db');
2752
2753 if ($type == 'edit') {
2754 $modul = '';
2755 $dd = '';
2756
2757 if ($mode == 'dd') {
2758 [$modul, $dd] = $this->split_dd_rid($rid);
2759 } else {
2760 $modul = (string)($record['dd_edit_modul'] ?? '');
2761 $dd = (string)($record['dd_edit_dd'] ?? '');
2762 }
2763
2764 if ($modul && $dd) {
2765 $editUrl = '?dbx_modul=dbxAdmin&dbx_run1=edit_dd&modul=' . rawurlencode($modul) . '&dd=' . rawurlencode($dd);
2766 $editTitle = $this->schema_texts()->format_fd_message('edit_dd', array('module' => $modul, 'dd' => $dd));
2767 $data['data']['edit_url'] = $this->esc($editUrl);
2768 $data['data']['edit_title'] = $this->esc($editTitle);
2769 $data['data']['edit_data_dBx'] = 'data-url="' . $this->esc($editUrl) . '" data-title="' . $this->esc($editTitle) . '"';
2770 } else {
2771 $data['data']['edit_url'] = '#';
2772 $data['data']['edit_title'] = $this->esc($this->schema_texts()->get_fd_message('no_dd_assigned'));
2773 $data['data']['edit_data_dBx'] = '';
2774 $data['data']['class'] = trim((string)($data['data']['class'] ?? '') . ' disabled text-muted');
2775 }
2776
2777 return $data;
2778 }
2779
2780 $showUrl = $this->append_url_params($base, array(
2781 'dbx_do' => 'row_show',
2782 'rid' => $rid,
2783 ));
2784 $deleteUrl = $this->append_url_params($base, array(
2785 'dbx_do' => 'row_delete',
2786 'rid' => $rid,
2787 ));
2788
2789 $data['data']['show_url'] = $this->esc($showUrl);
2790 $data['data']['delete_url'] = $this->esc($deleteUrl);
2791
2792 return $data;
2793 }
2794
2801 private function handle_dd_row_action(&$message) {
2802 $do = dbx()->get_modul_var('dbx_do', '', 'parameter');
2803 if (!in_array($do, array('row_show', 'row_delete'), true)) {
2804 return '';
2805 }
2806
2807 $rid = dbx()->get_modul_var('rid', '', 'parameter');
2808 [$modul, $dd] = $this->split_dd_rid($rid);
2809 if (!$modul || !$dd) {
2810 return '<div class="alert alert-danger">DD-Auswahl ist ungueltig.</div>';
2811 }
2812
2813 $ddRef = $this->dd_ref($modul, $dd);
2814
2815 if ($do == 'row_show') {
2816 $params = array('rid' => $rid);
2817 return $this->run_data_report(
2818 $ddRef,
2819 'DD Daten: ' . $modul . '|' . $dd,
2820 $this->build_url('dd', 'data_read', $params),
2821 $this->build_url('dd', 'data_save', $params),
2822 $this->build_url('dd', 'data_insert', $params),
2823 $this->build_url('dd', 'data_delete', $params)
2824 );
2825 }
2826
2827 if ($this->delete_dd_file($modul, $dd)) {
2828 $message = 'DD-Datei geloescht: ' . $modul . '|' . $dd;
2829 } else {
2830 $message = 'DD-Datei konnte nicht geloescht werden: ' . $modul . '|' . $dd;
2831 }
2832
2833 return '';
2834 }
2835
2842 private function handle_db_row_action(&$message) {
2843 $do = dbx()->get_modul_var('dbx_do', '', 'parameter');
2844 if (!in_array($do, array('row_show', 'row_delete'), true)) {
2845 return '';
2846 }
2847
2848 $rid = dbx()->get_modul_var('rid', '', 'parameter');
2849 [$server, $table] = $this->decode_db_rid($rid);
2850 if (!$server || !$table) {
2851 return '<div class="alert alert-danger">DB-Auswahl ist ungueltig.</div>';
2852 }
2853
2854 if ($do == 'row_show') {
2855 $ddRef = $this->ensure_db_view_dd($server, $table);
2856 if (!$ddRef) {
2857 return '<div class="alert alert-danger">Ansichts-DD konnte nicht erzeugt werden.</div>';
2858 }
2859
2860 $params = array('rid' => $rid);
2861 return $this->run_data_report(
2862 $ddRef,
2863 'DB Daten: ' . $server . '|' . $table,
2864 $this->build_url('db', 'data_read', $params),
2865 $this->build_url('db', 'data_save', $params),
2866 $this->build_url('db', 'data_insert', $params),
2867 $this->build_url('db', 'data_delete', $params)
2868 );
2869 }
2870
2871 $oDD = dbx()->get_system_obj('dbxDD');
2872 if ($oDD->drop_db_tab($server, $table)) {
2873 $message = 'DB-Tabelle geloescht: ' . $server . '|' . $table;
2874 } else {
2875 $message = 'DB-Tabelle konnte nicht geloescht werden: ' . $server . '|' . $table;
2876 }
2877
2878 return '';
2879 }
2880
2886 private function report_dd() {
2887 $texts = $this->schema_texts();
2888 $message = '';
2889 $content = $this->handle_dd_row_action($message);
2890 if ($content !== '') {
2891 return $content;
2892 }
2893
2894 $flds = array(
2895 'modul' => $texts->get_fd_message('column_module'),
2896 'dd' => $texts->get_fd_message('column_dd'),
2897 'path' => $texts->get_fd_message('column_path'),
2898 'server' => $texts->get_fd_message('column_server'),
2899 'database' => $texts->get_fd_message('column_database'),
2900 'table' => $texts->get_fd_message('column_table'),
2901 'dd_fields' => $texts->get_fd_message('column_dd_fields'),
2902 'db_fields' => $texts->get_fd_message('column_db_fields'),
2903 'count' => $texts->get_fd_message('column_records'),
2904 'sync' => $texts->get_fd_message('column_status'),
2905 'sync_info' => $texts->get_fd_message('column_details'),
2906 'act_sync' => $this->header_icon('bi bi-arrow-repeat', $texts->get_fd_message('action_dd_to_db')),
2907 'act_map' => $this->header_icon('bi bi-diagram-3', $texts->get_fd_message('action_mapping')),
2908 'act_move' => $this->header_icon('bi bi-box-arrow-right', $texts->get_fd_message('action_transfer')),
2909 'act_flds' => $this->header_icon('bi bi-list-columns', $texts->get_fd_message('action_dd_fields')),
2910 );
2911
2912 $batch = array(
2913 '0' => $texts->get_fd_message('batch_placeholder'),
2914 'batch_dd_to_db' => $texts->get_fd_message('batch_dd_to_db'),
2915 'batch_dd_to_db_force' => $texts->get_fd_message('batch_dd_to_db_force'),
2916 );
2917
2918 return $this->run_schema_report(
2919 'dd',
2920 $this->get_dd_records(),
2921 $flds,
2922 $this->build_url('dd', 'list_dd'),
2923 $batch,
2924 $message
2925 );
2926 }
2927
2933 private function report_db() {
2934 $texts = $this->schema_texts();
2935 $message = '';
2936 $content = $this->handle_db_row_action($message);
2937 if ($content !== '') {
2938 return $content;
2939 }
2940
2941 $flds = array(
2942 'modul' => $texts->get_fd_message('column_module'),
2943 'server' => $texts->get_fd_message('column_server'),
2944 'table' => $texts->get_fd_message('column_table'),
2945 'db_fields' => $texts->get_fd_message('column_db_fields'),
2946 'dd_fields' => $texts->get_fd_message('column_dd_fields'),
2947 'count' => $texts->get_fd_message('column_records'),
2948 'dd' => $texts->get_fd_message('column_dd'),
2949 'sync' => $texts->get_fd_message('column_status'),
2950 'sync_info' => $texts->get_fd_message('column_details'),
2951 'act_sync' => $this->header_icon('bi bi-arrow-down-up', $texts->get_fd_message('action_db_to_dd')),
2952 'act_dd_sync' => $this->header_icon('bi bi-arrow-repeat', $texts->get_fd_message('action_db_from_dd')),
2953 'act_map' => $this->header_icon('bi bi-diagram-3', $texts->get_fd_message('action_mapping')),
2954 'act_move' => $this->header_icon('bi bi-box-arrow-right', $texts->get_fd_message('action_transfer')),
2955 );
2956
2957 $batch = array(
2958 '0' => $texts->get_fd_message('batch_placeholder'),
2959 'batch_db_to_dd' => $texts->get_fd_message('batch_db_to_dd'),
2960 'batch_dd_to_db' => $texts->get_fd_message('batch_dd_to_db'),
2961 'batch_dd_to_db_force' => $texts->get_fd_message('batch_dd_to_db_force'),
2962 );
2963
2964 return $this->run_schema_report('db', $this->get_db_records($this->get_dd_records()), $flds, $this->build_url('db', 'list_db'), $batch, $message);
2965 }
2971 private function backup_restore_rows() {
2972 $rows = $this->get_db_records($this->get_dd_records());
2973 $texts = $this->schema_texts();
2974
2975 foreach ($rows as $no => $row) {
2976 [$server, $table] = $this->decode_db_rid((string)($row['rid'] ?? ''));
2977
2978 if (!$server) {
2979 $server = (string)($row['server'] ?? '');
2980 }
2981
2982 if (!$table) {
2983 $rows[$no]['backup'] = $this->badge($texts->get_fd_message('no_table'), 'secondary');
2984 $rows[$no]['act_backup'] = '';
2985 $rows[$no]['act_restore'] = '';
2986 continue;
2987 }
2988
2989 $rows[$no]['backup'] = $this->backup_label($server, $table);
2990
2991 $backupUrl = $this->build_url('dd', 'backup_db_table', array(
2992 'server' => $server,
2993 'table' => $table,
2994 'reset' => 1,
2995 ));
2996 $rows[$no]['act_backup'] = $this->openwin(
2997 $backupUrl,
2998 'bi bi-download',
2999 $texts->get_fd_message('action_backup_create'),
3000 980,
3001 700
3002 );
3003
3004 $restoreUrl = $this->build_url('dd', 'restore_db_table', array(
3005 'server' => $server,
3006 'table' => $table,
3007 ));
3008 $rows[$no]['act_restore'] = $this->openwin(
3009 $restoreUrl,
3010 'bi bi-upload',
3011 $texts->get_fd_message('action_restore_select'),
3012 980,
3013 760
3014 );
3015 }
3016
3017 return $rows;
3018 }
3019
3025 private function report_backup_db() {
3026 $texts = $this->schema_texts();
3027 $flds = array(
3028 'modul' => $texts->get_fd_message('column_module'),
3029 'server' => $texts->get_fd_message('column_server'),
3030 'database' => $texts->get_fd_message('column_database'),
3031 'table' => $texts->get_fd_message('column_table'),
3032 'count' => $texts->get_fd_message('column_records'),
3033 'path' => $texts->get_fd_message('column_path'),
3034 'backup' => $texts->get_fd_message('column_last_backup'),
3035 'act_backup' => $this->header_icon('bi bi-download', $texts->get_fd_message('action_backup_create')),
3036 );
3037
3038 $batch = array(
3039 '0' => $texts->get_fd_message('batch_placeholder'),
3040 'batch_backup_db' => $texts->get_fd_message('batch_backup_selected'),
3041 );
3042
3043 return $this->run_schema_report('backup', $this->backup_restore_rows(), $flds, $this->build_url('dd', 'backup_db'), $batch, '');
3044 }
3045
3051 private function report_restore_db() {
3052 $texts = $this->schema_texts();
3053 $flds = array(
3054 'modul' => $texts->get_fd_message('column_module'),
3055 'server' => $texts->get_fd_message('column_server'),
3056 'database' => $texts->get_fd_message('column_database'),
3057 'table' => $texts->get_fd_message('column_table'),
3058 'count' => $texts->get_fd_message('column_records'),
3059 'path' => $texts->get_fd_message('column_path'),
3060 'backup' => $texts->get_fd_message('column_last_backup'),
3061 'act_restore' => $this->header_icon('bi bi-upload', $texts->get_fd_message('action_restore_choose')),
3062 );
3063
3064 $batch = array(
3065 '0' => $texts->get_fd_message('batch_placeholder'),
3066 'batch_restore_latest_db' => $texts->get_fd_message('batch_restore_selected'),
3067 );
3068
3069 return $this->run_schema_report('restore', $this->backup_restore_rows(), $flds, $this->build_url('dd', 'restore_db'), $batch, '');
3070 }
3071
3079 private function append_url_params($url, $params = array()) {
3080 if (!$url) {
3081 return '';
3082 }
3083
3084 foreach ($params as $key => $value) {
3085 $url .= (strpos($url, '?') === false ? '?' : '&')
3086 . rawurlencode((string)$key) . '=' . rawurlencode((string)$value);
3087 }
3088
3089 return $url;
3090 }
3091
3098 private function process_status_label($status) {
3099 $map = array(
3100 'new' => 'Neu',
3101 'reset' => 'Neu',
3102 'running' => 'Laeuft',
3103 'paused' => 'Angehalten',
3104 'canceled' => 'Abgebrochen',
3105 'finished' => 'Fertig',
3106 'error' => 'Fehler',
3107 );
3108
3109 return $map[$status] ?? $status;
3110 }
3111
3118 private function process_status_class($status) {
3119 $map = array(
3120 'running' => 'bg-primary',
3121 'paused' => 'bg-warning text-dark',
3122 'canceled' => 'bg-danger',
3123 'finished' => 'bg-success',
3124 'error' => 'bg-danger',
3125 'new' => 'bg-secondary',
3126 'reset' => 'bg-secondary',
3127 );
3128
3129 return $map[$status] ?? 'bg-secondary';
3130 }
3131
3138 private function process_status_icon($status) {
3139 $map = array(
3140 'running' => 'bi bi-play-fill',
3141 'paused' => 'bi bi-pause-fill',
3142 'canceled' => 'bi bi-x-lg',
3143 'finished' => 'bi bi-check-lg',
3144 'error' => 'bi bi-exclamation-triangle',
3145 'new' => 'bi bi-circle',
3146 'reset' => 'bi bi-arrow-clockwise',
3147 );
3148
3149 return $map[$status] ?? 'bi bi-circle';
3150 }
3151
3158 private function process_type_label($type) {
3159 $map = array(
3160 'sync_dd_to_db' => 'DD -> DB',
3161 'sync_db_to_dd' => 'DB -> DD',
3162 'transfer_table' => 'Transfer',
3163 'backup' => 'Backup',
3164 'restore' => 'Restore',
3165 'schema_batch' => 'Batch',
3166 );
3167
3168 return $map[$type] ?? ($type ?: 'Prozess');
3169 }
3170
3177 private function process_task_label($phase) {
3178 $phase = (string)$phase;
3179
3180 if (strpos($phase, 'backup') !== false) return 'Aufgabe Backup';
3181 if (strpos($phase, 'restore') !== false) return 'Aufgabe Restore';
3182 if (strpos($phase, 'create') !== false || strpos($phase, 'rename') !== false || strpos($phase, 'drop') !== false) return 'Aufgabe Struktur';
3183 if (strpos($phase, 'add_') === 0) return 'Aufgabe Schema';
3184 if (strpos($phase, 'read') !== false || strpos($phase, 'merge') !== false || strpos($phase, 'write') !== false) return 'Aufgabe DD';
3185 if (strpos($phase, 'prepare') !== false) return 'Aufgabe Vorbereitung';
3186 if (strpos($phase, 'batch') !== false) return 'Aufgabe Batch';
3187
3188 return 'Aufgabe';
3189 }
3190
3197 private function process_step_label($phase) {
3198 $map = array(
3199 'prepare' => 'Vorbereiten',
3200 'prepare_target' => 'Ziel vorbereiten',
3201 'create_table' => 'Tabelle anlegen',
3202 'add_fields' => 'Felder ergaenzen',
3203 'add_indexes' => 'Indizes ergaenzen',
3204 'backup_old' => 'Backup erstellen',
3205 'backup_source' => 'Quelle sichern',
3206 'rename_old' => 'Alte Tabelle umbenennen',
3207 'create_new' => 'Neue Tabelle anlegen',
3208 'restore_new' => 'Daten einlesen',
3209 'restore_target' => 'Ziel einlesen',
3210 'drop_old' => 'Alte Tabelle entfernen',
3211 'read_schema' => 'Schema lesen',
3212 'merge_meta' => 'DD zusammenfuehren',
3213 'write_dd' => 'DD schreiben',
3214 'batch_step' => 'Eintrag bearbeiten',
3215 );
3216
3217 return $map[$phase] ?? ($phase ?: 'Schritt');
3218 }
3219
3226 private function process_message_label($message) {
3227 $message = (string)$message;
3228 $map = array(
3229 'process paused' => 'Prozess angehalten',
3230 'process resumed' => 'Prozess fortgesetzt',
3231 'process continued' => 'Prozess fortgesetzt',
3232 'process canceled' => 'Prozess abgebrochen',
3233 'process restarted' => 'Prozess neu gestartet',
3234 'sync initialized' => 'Synchronisierung vorbereitet',
3235 'transfer initialized' => 'Transfer vorbereitet',
3236 'schema loaded' => 'Schema gelesen',
3237 'meta merged' => 'DD-Informationen zusammengefuehrt',
3238 'sync dd -> db finished' => 'DD -> DB abgeschlossen',
3239 'sync dd -> db rebuild finished' => 'DD -> DB Rebuild abgeschlossen',
3240 'sync db -> dd finished' => 'DB -> DD abgeschlossen',
3241 'transfer finished' => 'Transfer abgeschlossen',
3242 'batch initialized' => 'Batch vorbereitet',
3243 'batch finished' => 'Batch abgeschlossen',
3244 'backup finished' => 'Backup abgeschlossen',
3245 'restore finished' => 'Restore abgeschlossen',
3246 );
3247
3248 return $map[$message] ?? $message;
3249 }
3250
3260 private function render_process($title, $state, $nextUrl = '', $backUrl = '') {
3261 $oTPL = dbx()->get_system_obj('dbxTPL');
3262 $status = $state['status'] ?? 'running';
3263 $percent = (int)($state['percent'] ?? 0);
3264 if ($percent < 0) $percent = 0;
3265 if ($percent > 100) $percent = 100;
3266
3267 $stepPercent = (int)($state['step_percent'] ?? $percent);
3268 if ($stepPercent < 0) $stepPercent = 0;
3269 if ($stepPercent > 100) $stepPercent = 100;
3270
3271 $barClass = 'bg-primary';
3272 if ($status == 'finished') $barClass = 'bg-success';
3273 if ($status == 'error' || $status == 'canceled') $barClass = 'bg-danger';
3274 if ($status == 'paused') $barClass = 'bg-warning';
3275
3276 $restartUrl = $nextUrl ? $this->append_url_params($nextUrl, array('reset' => 1, 'proc_cmd' => 'restart')) : '';
3277 $targetId = 'dbx_process_' . substr(md5((string)($state['proc_key'] ?? $title)), 0, 14);
3278 $phase = (string)($state['phase'] ?? '');
3279 $procType = (string)($state['proc_type'] ?? ($state['act'] ?? 'schema_batch'));
3280 $hasControls = $nextUrl ? 1 : 0;
3281 $autostart = ($nextUrl && $status == 'running') ? 1 : 0;
3282
3283 $data = array(
3284 'target_id' => $this->esc($targetId),
3285 'title' => $this->esc($title),
3286 'status_key' => $this->esc($status),
3287 'status_label' => $this->esc($this->process_status_label($status)),
3288 'status_class' => $this->esc($this->process_status_class($status)),
3289 'status_icon' => $this->esc($this->process_status_icon($status)),
3290 'message' => $this->esc($this->process_message_label($state['message'] ?? '')),
3291 'percent' => $percent,
3292 'step_percent' => $stepPercent,
3293 'bar_class' => $this->esc($barClass),
3294 'task_label' => $this->esc($this->process_task_label($phase)),
3295 'step_label' => $this->esc($this->process_step_label($phase)),
3296 'process_label' => $this->esc($this->process_type_label($procType)),
3297 'updated_at' => $this->esc($state['updated_at'] ?? ''),
3298 'next_url' => $this->esc($nextUrl),
3299 'pause_url' => $this->esc($this->append_url_params($nextUrl, array('proc_cmd' => 'pause'))),
3300 'resume_url' => $this->esc($this->append_url_params($nextUrl, array('proc_cmd' => 'resume'))),
3301 'continue_url' => $this->esc($this->append_url_params($nextUrl, array('proc_cmd' => 'continue'))),
3302 'cancel_url' => $this->esc($this->append_url_params($nextUrl, array('proc_cmd' => 'cancel'))),
3303 'restart_url' => $this->esc($restartUrl),
3304 'autostart' => $autostart,
3305 'interval' => 800,
3306 'pause_visible' => $hasControls ? 'running' : '_none',
3307 'resume_visible' => $hasControls ? 'paused' : '_none',
3308 'continue_visible' => $hasControls ? 'canceled' : '_none',
3309 'restart_visible' => $restartUrl ? 'paused,canceled,error,finished' : '_none',
3310 'cancel_visible' => $hasControls ? 'running,paused' : '_none',
3311 'back_url' => $this->esc($backUrl),
3312 );
3313
3314 return $oTPL->get_tpl('dbxAdmin|schema-process', $data);
3315 }
3316
3322 private function run_sync_dd_to_db() {
3323 $modul = dbx()->get_modul_var('modul', 'dbx', 'parameter');
3324 $dd = dbx()->get_modul_var('dd', '', 'parameter');
3325 $mode = dbx()->get_modul_var('mode', 'apply', 'parameter');
3326 $reset = dbx()->get_modul_var('reset', 0, 'int');
3327 $cmd = dbx()->get_modul_var('proc_cmd', '', 'parameter');
3328 $oDD = dbx()->get_system_obj('dbxDD');
3329
3330 if (!$dd) {
3331 return '<div class="alert alert-danger">DD fehlt.</div>';
3332 }
3333
3334 if ($reset) {
3335 $oDD->sync_dd_to_db($modul, $dd, 'reset');
3336 }
3337
3338 $nextUrl = $this->build_url('dd', 'sync_dd_to_db', array(
3339 'modul' => $modul,
3340 'dd' => $dd,
3341 'mode' => $mode,
3342 ));
3343
3344 if ($cmd && in_array($cmd, array('pause', 'resume', 'continue', 'cancel'), true)) {
3345 $state = $oDD->sync_dd_to_db($modul, $dd, $cmd);
3346 return $this->render_process('DD -> DB: ' . $modul . '|' . $dd, $state, $nextUrl, $this->build_url('dd', 'list_dd'));
3347 }
3348
3349 $state = $oDD->sync_dd_to_db($modul, $dd, $mode);
3350
3351 $content = $this->render_process('DD -> DB: ' . $modul . '|' . $dd, $state, $nextUrl, $this->build_url('dd', 'list_dd'));
3352
3353 if (($state['status'] ?? '') == 'error' && strpos((string)($state['message'] ?? ''), 'rebuild needed') !== false) {
3354 $force = $this->build_url('dd', 'sync_dd_to_db', array(
3355 'modul' => $modul,
3356 'dd' => $dd,
3357 'mode' => 'force',
3358 'reset' => 1,
3359 ));
3360 $content .= '<div class="mt-3"><a class="btn btn-danger" href="' . $this->esc($force) . '">Rebuild starten</a></div>';
3361 }
3362
3363 return $content;
3364 }
3365
3371 private function run_sync_db_to_dd() {
3372 $server = dbx()->get_modul_var('server', '', 'parameter');
3373 $table = dbx()->get_modul_var('table', '', 'parameter');
3374 $modul = dbx()->get_modul_var('modul', '', 'parameter');
3375 $dd = dbx()->get_modul_var('dd', '', 'parameter');
3376 $mode = dbx()->get_modul_var('mode', 'merge', 'parameter');
3377 $reset = dbx()->get_modul_var('reset', 0, 'int');
3378 $cmd = dbx()->get_modul_var('proc_cmd', '', 'parameter');
3379
3380 if (!$server || !$table) {
3381 return '<div class="alert alert-danger">Server oder Tabelle fehlt.</div>';
3382 }
3383
3384 if (!$modul || !$dd) {
3385 return $this->form_db_to_dd($server, $table);
3386 }
3387
3388 $oDD = dbx()->get_system_obj('dbxDD');
3389 if ($reset) {
3390 $oDD->sync_db_to_dd($modul, $dd, 'reset', $server, $table);
3391 }
3392
3393 $nextUrl = $this->build_url('db', 'sync_db_to_dd', array(
3394 'server' => $server,
3395 'table' => $table,
3396 'modul' => $modul,
3397 'dd' => $dd,
3398 'mode' => $mode,
3399 ));
3400
3401 if ($cmd && in_array($cmd, array('pause', 'resume', 'continue', 'cancel'), true)) {
3402 $state = $oDD->sync_db_to_dd($modul, $dd, $cmd, $server, $table);
3403 return $this->render_process('DB -> DD: ' . $server . '|' . $table . ' -> ' . $modul . '|' . $dd, $state, $nextUrl, $this->build_url('db', 'list_db'));
3404 }
3405
3406 $state = $oDD->sync_db_to_dd($modul, $dd, $mode, $server, $table);
3407
3408 return $this->render_process('DB -> DD: ' . $server . '|' . $table . ' -> ' . $modul . '|' . $dd, $state, $nextUrl, $this->build_url('db', 'list_db'));
3409 }
3410
3418 private function form_db_to_dd($server, $table) {
3419 $oForm = dbx()->get_system_obj('dbxForm');
3420 $oForm->init('form-schema-action');
3421 $oForm->_fd = 'dbxAdmin|schema-report';
3422 $oForm->load_fd_messages();
3423 $oForm->_data = array(
3424 'server' => $server,
3425 'table' => $table,
3426 'modul' => 'dbx',
3427 'dd' => $this->sanitize_dd_name($table),
3428 );
3429 $oForm->_action = $this->build_url('db', 'sync_db_to_dd', array(
3430 'server' => $server,
3431 'table' => $table,
3432 ));
3433 $oForm->_msg_info = $oForm->get_fd_message('db_to_dd_info');
3434
3435 $oForm->add_fld('server', 'hidden', rules: 'parameter');
3436 $oForm->add_fld('table', 'hidden', rules: 'parameter');
3437 $oForm->add_fld(
3438 'modul',
3439 'select-single-label',
3440 label: $oForm->get_fd_message('label_module'),
3441 rules: 'parameter',
3442 options: $this->get_module_options()
3443 );
3444 $oForm->add_fld(
3445 'dd',
3446 'text-label',
3447 label: $oForm->get_fd_message('column_dd'),
3448 rules: 'parameter|min=1'
3449 );
3450
3451 if ($oForm->submit() && !$oForm->errors()) {
3452 $modul = $oForm->get_post('modul', 'dbx', 'parameter');
3453 $dd = $this->sanitize_dd_name($oForm->get_post('dd', $table, 'parameter'));
3454
3455 dbx()->set_modul_var('modul', $modul);
3456 dbx()->set_modul_var('dd', $dd);
3457 dbx()->set_modul_var('mode', 'merge');
3458 dbx()->set_modul_var('reset', 1);
3459
3460 return $this->run_sync_db_to_dd();
3461 }
3462
3463 return $oForm->run();
3464 }
3465
3471 private function run_mapping_editor() {
3472 $texts = $this->schema_texts();
3473 $kind = dbx()->get_modul_var('kind', 'dd_to_db', 'parameter');
3474 $modul = dbx()->get_modul_var('modul', '', 'parameter');
3475 $dd = dbx()->get_modul_var('dd', '', 'parameter');
3476 $server = dbx()->get_modul_var('server', '', 'parameter');
3477 $table = dbx()->get_modul_var('table', '', 'parameter');
3478
3479 if (!$dd && $kind !== 'transfer') {
3480 return '<div class="alert alert-danger">'
3481 . $this->esc($texts->get_fd_message('missing_dd'))
3482 . '</div>';
3483 }
3484
3485 $oDD = dbx()->get_system_obj('dbxDD');
3486 $context = array(
3487 'modul' => $modul,
3488 'dd' => $dd,
3489 'server' => $server,
3490 'table' => $table,
3491 );
3492
3493 $model = $oDD->build_schema_mapping($kind, $context);
3494
3495 $oForm = dbx()->get_system_obj('dbxForm');
3496 $oForm->init('schema-mapping');
3497 $oForm->_fd = 'dbxAdmin|schema-report';
3498 $oForm->load_fd_messages();
3499 $oForm->_msg_info = '';
3500 $oForm->_action = $this->build_url(($kind == 'db_to_dd') ? 'db' : 'dd', 'mapping', array(
3501 'kind' => $kind,
3502 'modul' => $modul,
3503 'dd' => $dd,
3504 'server' => $server,
3505 'table' => $table,
3506 ));
3507
3508 $oForm->_data = array(
3509 'kind' => $kind,
3510 'modul' => $modul,
3511 'dd' => $dd,
3512 'server' => $server,
3513 'table' => $table,
3514 );
3515
3516 $oForm->add_fld('kind', 'hidden', rules: 'parameter');
3517 $oForm->add_fld('modul', 'hidden', rules: 'parameter');
3518 $oForm->add_fld('dd', 'hidden', rules: 'parameter');
3519 $oForm->add_fld('server', 'hidden', rules: 'parameter');
3520 $oForm->add_fld('table', 'hidden', rules: 'parameter');
3521
3522 if ($oForm->submit() && !$oForm->errors()) {
3523 $mapping = $this->posted_mapping_from_model($model);
3524 $ok = $oDD->save_schema_mapping($kind, $model['context'] ?? $context, $mapping);
3525
3526 if ($ok) {
3527 $oForm->_msg_success = $oForm->get_fd_message('mapping_saved');
3528 $model = $oDD->build_schema_mapping($kind, $model['context'] ?? $context);
3529 } else {
3530 $oForm->_msg_error = $oForm->get_fd_message('mapping_save_error');
3531 }
3532 }
3533
3534 $oForm->add_obj('mapping_board', 'obj-value', $this->render_mapping_board($model));
3535 $oForm->add_obj('save_button', 'obj-value',
3536 '<button type="submit" class="btn btn-primary" title="'
3537 . $this->esc($oForm->get_fd_message('action_save'))
3538 . '"><i class="bi bi-save"></i></button>'
3539 );
3540 $oForm->add_obj('back_button', 'obj-value',
3541 '<a class="btn btn-secondary" href="' . $this->esc($this->build_url(($kind == 'db_to_dd') ? 'db' : 'dd', ($kind == 'db_to_dd') ? 'list_db' : 'list_dd')) . '" title="'
3542 . $this->esc($oForm->get_fd_message('action_back'))
3543 . '"><i class="bi bi-arrow-left"></i></a>'
3544 );
3545
3546 return $oForm->run();
3547 }
3548
3554 private function run_transfer() {
3555 $texts = $this->schema_texts();
3556 $sourceServer = dbx()->get_modul_var('source_server', '', 'parameter');
3557 $sourceTable = dbx()->get_modul_var('source_table', '', 'parameter');
3558 $start = dbx()->get_modul_var('start', 0, 'int');
3559
3560 if (!$sourceServer || !$sourceTable) {
3561 return '<div class="alert alert-danger">'
3562 . $this->esc($texts->get_fd_message('missing_source'))
3563 . '</div>';
3564 }
3565
3566 if (!$start) {
3567 return $this->form_transfer($sourceServer, $sourceTable);
3568 }
3569
3570 $targetServer = dbx()->get_modul_var('target_server', '', 'parameter');
3571 $targetTable = dbx()->get_modul_var('target_table', $sourceTable, 'parameter');
3572 $createTarget = dbx()->get_modul_var('create_target', 1, 'int');
3573 $truncate = dbx()->get_modul_var('truncate_target', 1, 'int');
3574 $reset = dbx()->get_modul_var('reset', 0, 'int');
3575 $cmd = dbx()->get_modul_var('proc_cmd', '', 'parameter');
3576 $oDD = dbx()->get_system_obj('dbxDD');
3577
3578 if ($reset) {
3579 $oDD->transfer_table($sourceServer, $sourceTable, $targetServer, $targetTable, 'reset', $createTarget, $truncate);
3580 }
3581
3582 $nextUrl = $this->build_url('db', 'transfer', array(
3583 'source_server' => $sourceServer,
3584 'source_table' => $sourceTable,
3585 'target_server' => $targetServer,
3586 'target_table' => $targetTable,
3587 'create_target' => $createTarget,
3588 'truncate_target' => $truncate,
3589 'start' => 1,
3590 ));
3591
3592 if ($cmd && in_array($cmd, array('pause', 'resume', 'continue', 'cancel'), true)) {
3593 $state = $oDD->transfer_table($sourceServer, $sourceTable, $targetServer, $targetTable, $cmd, $createTarget, $truncate);
3594 return $this->render_process('Transfer: ' . $sourceServer . '|' . $sourceTable . ' -> ' . $targetServer . '|' . $targetTable, $state, $nextUrl, $this->build_url('db', 'list_db'));
3595 }
3596
3597 $state = $oDD->transfer_table($sourceServer, $sourceTable, $targetServer, $targetTable, 'step', $createTarget, $truncate);
3598
3599 return $this->render_process('Transfer: ' . $sourceServer . '|' . $sourceTable . ' -> ' . $targetServer . '|' . $targetTable, $state, $nextUrl, $this->build_url('db', 'list_db'));
3600 }
3601
3609 private function form_transfer($sourceServer, $sourceTable) {
3610 $oForm = dbx()->get_system_obj('dbxForm');
3611 $servers = $this->get_server_options();
3612
3613 $defaultTarget = '';
3614 foreach ($servers as $server => $label) {
3615 if ($server != $sourceServer) {
3616 $defaultTarget = $server;
3617 break;
3618 }
3619 }
3620 if (!$defaultTarget) {
3621 $defaultTarget = $sourceServer;
3622 }
3623
3624 $oForm->init('form-schema-action');
3625 $oForm->_fd = 'dbxAdmin|schema-report';
3626 $oForm->load_fd_messages();
3627 $oForm->_data = array(
3628 'source_server' => $sourceServer,
3629 'source_table' => $sourceTable,
3630 'target_server' => $defaultTarget,
3631 'target_table' => $sourceTable,
3632 'create_target' => 1,
3633 'truncate_target' => 1,
3634 );
3635 $oForm->_action = $this->build_url('db', 'transfer', array(
3636 'source_server' => $sourceServer,
3637 'source_table' => $sourceTable,
3638 ));
3639 $oForm->_msg_info = $oForm->get_fd_message('transfer_info');
3640
3641 $yesNo = array(
3642 1 => $oForm->get_fd_message('yes'),
3643 0 => $oForm->get_fd_message('no'),
3644 );
3645 $oForm->add_fld('source_server', 'text-label', label: $oForm->get_fd_message('label_source_server'), rules: 'parameter');
3646 $oForm->add_fld('source_table', 'text-label', label: $oForm->get_fd_message('label_source_table'), rules: 'parameter');
3647 $oForm->add_fld('target_server', 'select-single-label', label: $oForm->get_fd_message('label_target_server'), rules: 'parameter', options: $servers);
3648 $oForm->add_fld('target_table', 'text-label', label: $oForm->get_fd_message('label_target_table'), rules: 'parameter|min=1');
3649 $oForm->add_fld('create_target', 'select-single-label', label: $oForm->get_fd_message('label_create_target'), rules: 'int', options: $yesNo);
3650 $oForm->add_fld('truncate_target', 'select-single-label', label: $oForm->get_fd_message('label_truncate_target'), rules: 'int', options: $yesNo);
3651
3652 if ($oForm->submit() && !$oForm->errors()) {
3653 dbx()->set_modul_var('target_server', $oForm->get_post('target_server', $defaultTarget, 'parameter'));
3654 dbx()->set_modul_var('target_table', $oForm->get_post('target_table', $sourceTable, 'parameter'));
3655 dbx()->set_modul_var('create_target', $oForm->get_post('create_target', 1, 'int'));
3656 dbx()->set_modul_var('truncate_target', $oForm->get_post('truncate_target', 1, 'int'));
3657 dbx()->set_modul_var('start', 1);
3658 dbx()->set_modul_var('reset', 1);
3659
3660 return $this->run_transfer();
3661 }
3662
3663 return $oForm->run();
3664 }
3665
3671 private function run_backup_db_table() {
3672 $texts = $this->schema_texts();
3673 $server = dbx()->get_modul_var('server', '', 'parameter');
3674 $table = dbx()->get_modul_var('table', '', 'parameter');
3675 if (!$server || !$table) {
3676 return '<div class="alert alert-danger">' . $this->esc($texts->get_fd_message('missing_server_or_table')) . '</div>';
3677 }
3678
3679 $result = $this->backup_table($server, $table);
3680 $ok = !empty($result['ok']);
3681 $state = array(
3682 'proc_type' => 'backup',
3683 'proc_key' => 'backup_' . $server . '_' . $table,
3684 'status' => $ok ? 'finished' : 'error',
3685 'phase' => 'backup_source',
3686 'message' => $ok ? 'backup finished' : ($result['msg'] ?? 'backup error'),
3687 'percent' => $ok ? 100 : 0,
3688 'step_percent' => $ok ? 100 : 0,
3689 'updated_at' => date('Y-m-d H:i:s'),
3690 );
3691
3692 if ($ok) {
3693 $state['message'] = 'backup finished: ' . $this->backup_relative_file($result['file'] ?? '');
3694 }
3695
3696 return $this->render_process('Backup: ' . $server . '|' . $table, $state, '', $this->build_url('dd', 'backup_db'));
3697 }
3698
3706 private function form_restore_db_table($server, $table) {
3707 $texts = $this->schema_texts();
3708 $backups = $this->table_backups($server, $table);
3709 if (!$backups) {
3710 return '<div class="alert alert-warning">' . $this->esc(
3711 $texts->format_fd_message('no_backup_for_table', array('table' => $server . '|' . $table))
3712 ) . '</div>';
3713 }
3714
3715 $options = array('latest' => $texts->get_fd_message('latest_backup'));
3716 foreach ($backups as $backup) {
3717 $file = (string)($backup['_file'] ?? '');
3718 if ($file === '') {
3719 continue;
3720 }
3721 $options[$file] = (string)($backup['created_at'] ?? $file) . ' / '
3722 . $texts->format_fd_message('record_count_short', array('count' => (int)($backup['count'] ?? 0)));
3723 }
3724
3725 $oForm = dbx()->get_system_obj('dbxForm');
3726 $oForm->init('form-schema-restore');
3727 $oForm->_fd = 'dbxAdmin|schema-report';
3728 $oForm->load_fd_messages();
3729 $oForm->_data = array(
3730 'server' => $server,
3731 'table' => $table,
3732 'backup_file' => 'latest',
3733 );
3734 $oForm->_action = $this->build_url('dd', 'restore_db_table', array(
3735 'server' => $server,
3736 'table' => $table,
3737 ));
3738 $oForm->_msg_info = $texts->get_fd_message('restore_warning');
3739
3740 $oForm->add_fld('server', 'text-label', label: $texts->get_fd_message('column_server'), rules: 'parameter');
3741 $oForm->add_fld('table', 'text-label', label: $texts->get_fd_message('column_table'), rules: 'parameter');
3742 $oForm->add_fld('backup_file', 'select-single-label', label: $texts->get_fd_message('label_backup'), rules: 'parameter+.-_', options: $options);
3743 $oForm->add_obj('restore_submit', 'dbx|button-submit', data: 'label=' . $texts->get_fd_message('action_restore_start'));
3744
3745 if ($oForm->submit() && !$oForm->errors()) {
3746 dbx()->set_modul_var('backup_file', $oForm->get_post('backup_file', 'latest', 'parameter+.-_'));
3747 dbx()->set_modul_var('start', 1);
3748 return $this->run_restore_db_table();
3749 }
3750
3751 return $oForm->run();
3752 }
3753
3759 private function run_restore_db_table() {
3760 $texts = $this->schema_texts();
3761 $server = dbx()->get_modul_var('server', '', 'parameter');
3762 $table = dbx()->get_modul_var('table', '', 'parameter');
3763 $start = dbx()->get_modul_var('start', 0, 'int');
3764 if (!$server || !$table) {
3765 return '<div class="alert alert-danger">' . $this->esc($texts->get_fd_message('missing_server_or_table')) . '</div>';
3766 }
3767
3768 if (!$start) {
3769 return $this->form_restore_db_table($server, $table);
3770 }
3771
3772 $file = dbx()->get_modul_var('backup_file', 'latest', 'parameter+.-_');
3773 if ($file === 'latest') {
3774 $latest = $this->latest_backup($server, $table);
3775 $file = (string)($latest['_file'] ?? '');
3776 }
3777
3778 $result = $file
3779 ? $this->restore_table_from_backup($file)
3780 : array('ok' => 0, 'msg' => $texts->get_fd_message('no_backup_found'));
3781 $ok = !empty($result['ok']);
3782 $state = array(
3783 'proc_type' => 'restore',
3784 'proc_key' => 'restore_' . $server . '_' . $table,
3785 'status' => $ok ? 'finished' : 'error',
3786 'phase' => 'restore_target',
3787 'message' => $ok ? 'restore finished' : ($result['msg'] ?? 'restore error'),
3788 'percent' => $ok ? 100 : 0,
3789 'step_percent' => $ok ? 100 : 0,
3790 'updated_at' => date('Y-m-d H:i:s'),
3791 );
3792
3793 if ($ok) {
3794 $state['message'] = 'restore finished: ' . $this->backup_relative_file($result['file'] ?? $file);
3795 }
3796
3797 return $this->render_process('Restore: ' . $server . '|' . $table, $state, '', $this->build_url('dd', 'restore_db'));
3798 }
3799
3807 private function start_batch($act, $selects) {
3808 $oDD = dbx()->get_system_obj('dbxDD');
3809
3810 foreach ($selects as $selected) {
3811 if ($act == 'batch_dd_to_db' || $act == 'batch_dd_to_db_force') {
3812 $parts = $this->selected_dd_parts($selected);
3813 if (count($parts) != 2) {
3814 continue;
3815 }
3816
3817 $oDD->sync_dd_to_db($parts[0], $parts[1], 'reset');
3818 }
3819
3820 if ($act == 'batch_db_to_dd') {
3821 $parts = $this->decode_db_rid($selected);
3822 if (!$parts[0] || !$parts[1]) {
3823 continue;
3824 }
3825
3826 $ddIndex = $this->get_dd_index_by_db($this->get_dd_records());
3827 $dds = $this->get_dd_records_for_db($ddIndex, $parts[0], $parts[1]);
3828
3829 if ($dds) {
3830 $oDD->sync_db_to_dd($dds[0]['modul'], $dds[0]['dd'], 'reset', $parts[0], $parts[1]);
3831 } else {
3832 $oDD->sync_db_to_dd('dbx', $this->sanitize_dd_name($parts[1]), 'reset', $parts[0], $parts[1]);
3833 }
3834 }
3835 }
3836
3837 $back = 'db';
3838 if ($act == 'batch_backup_db') $back = 'backup';
3839 if ($act == 'batch_restore_latest_db') $back = 'restore';
3840
3841 $state = array(
3842 'proc_type' => 'schema_batch',
3843 'proc_key' => 'schema_batch',
3844 'act' => $act,
3845 'selects' => array_values($selects),
3846 'pos' => 0,
3847 'status' => 'running',
3848 'phase' => 'batch_step',
3849 'message' => 'batch initialized',
3850 'percent' => 0,
3851 'step_percent' => 0,
3852 'back' => $back,
3853 );
3854
3855 dbx()->set_remember_var('schema_batch', $state, 'dbxAdmin');
3856 return $this->run_batch();
3857 }
3858
3865 private function selected_dd_parts($selected) {
3866 $selected = (string)$selected;
3867
3868 if (str_starts_with($selected, 'db_')) {
3869 [$server, $table] = $this->decode_db_rid($selected);
3870 if (!$server || !$table) {
3871 return array();
3872 }
3873
3874 $ddIndex = $this->get_dd_index_by_db($this->get_dd_records());
3875 $dds = $this->get_dd_records_for_db($ddIndex, $server, $table);
3876 if (!$dds) {
3877 return array();
3878 }
3879
3880 return array($dds[0]['modul'], $dds[0]['dd']);
3881 }
3882
3883 $parts = explode('|', $selected, 2);
3884 return (count($parts) == 2) ? $parts : array();
3885 }
3886
3893 private function reset_batch_children($state) {
3894 if (!is_array($state) || empty($state['selects'])) {
3895 return;
3896 }
3897
3898 $oDD = dbx()->get_system_obj('dbxDD');
3899 $act = $state['act'] ?? '';
3900
3901 foreach ($state['selects'] as $selected) {
3902 if ($act == 'batch_dd_to_db' || $act == 'batch_dd_to_db_force') {
3903 $parts = $this->selected_dd_parts($selected);
3904 if (count($parts) != 2) {
3905 continue;
3906 }
3907
3908 $oDD->sync_dd_to_db($parts[0], $parts[1], 'reset');
3909 continue;
3910 }
3911
3912 if ($act == 'batch_db_to_dd') {
3913 $parts = $this->decode_db_rid($selected);
3914 if (!$parts[0] || !$parts[1]) {
3915 continue;
3916 }
3917
3918 $ddIndex = $this->get_dd_index_by_db($this->get_dd_records());
3919 $dds = $this->get_dd_records_for_db($ddIndex, $parts[0], $parts[1]);
3920 $modul = $dds ? $dds[0]['modul'] : 'dbx';
3921 $dd = $dds ? $dds[0]['dd'] : $this->sanitize_dd_name($parts[1]);
3922 $oDD->sync_db_to_dd($modul, $dd, 'reset', $parts[0], $parts[1]);
3923 }
3924 }
3925 }
3926
3933 private function batch_back_url($backRun) {
3934 if ($backRun == 'db') {
3935 return $this->build_url('db', 'list_db');
3936 }
3937 if ($backRun == 'backup') {
3938 return $this->build_url('dd', 'backup_db');
3939 }
3940 if ($backRun == 'restore') {
3941 return $this->build_url('dd', 'restore_db');
3942 }
3943 return $this->build_url('dd', 'list_dd');
3944 }
3945
3953 private function control_batch($state, $cmd) {
3954 $cmd = strtolower(trim((string)$cmd));
3955 $status = $state['status'] ?? 'running';
3956
3957 if ($cmd == 'pause' && !in_array($status, array('finished', 'error', 'canceled'), true)) {
3958 $state['status'] = 'paused';
3959 $state['message'] = 'process paused';
3960 } elseif (($cmd == 'resume' || $cmd == 'continue') && in_array($status, array('paused', 'canceled'), true)) {
3961 $state['status'] = 'running';
3962 $state['message'] = ($cmd == 'continue') ? 'process continued' : 'process resumed';
3963 } elseif ($cmd == 'cancel' && !in_array($status, array('finished', 'error'), true)) {
3964 $state['status'] = 'canceled';
3965 $state['message'] = 'process canceled';
3966 } elseif ($cmd == 'restart') {
3967 $this->reset_batch_children($state);
3968 $state['pos'] = 0;
3969 $state['status'] = 'running';
3970 $state['phase'] = 'batch_step';
3971 $state['percent'] = 0;
3972 $state['step_percent'] = 0;
3973 $state['message'] = 'process restarted';
3974 }
3975
3976 $state['updated_at'] = date('Y-m-d H:i:s');
3977 dbx()->set_remember_var('schema_batch', $state, 'dbxAdmin');
3978 return $state;
3979 }
3980
3986 private function run_batch() {
3987 $state = dbx()->get_remember_var('schema_batch', array(), 'dbxAdmin');
3988 if (!is_array($state) || empty($state['selects'])) {
3989 return '<div class="alert alert-warning">Kein Batch aktiv.</div>';
3990 }
3991
3992 $cmd = dbx()->get_modul_var('proc_cmd', '', 'parameter');
3993 if ($cmd && in_array($cmd, array('pause', 'resume', 'continue', 'cancel', 'restart'), true)) {
3994 $state = $this->control_batch($state, $cmd);
3995 }
3996
3997 $total = count($state['selects']);
3998 $pos = (int)($state['pos'] ?? 0);
3999 $backRun = (string)($state['back'] ?? 'dd');
4000 if (!in_array($backRun, array('dd', 'db', 'backup', 'restore'), true)) {
4001 $backRun = 'dd';
4002 }
4003 $nextUrl = $this->build_url('dd', 'batch');
4004 $backUrl = $this->batch_back_url($backRun);
4005
4006 if (in_array(($state['status'] ?? ''), array('paused', 'canceled', 'error'), true)) {
4007 return $this->render_process('Batch', $state, $nextUrl, $backUrl);
4008 }
4009
4010 if ($pos >= $total) {
4011 $state['status'] = 'finished';
4012 $state['message'] = 'batch finished';
4013 $state['percent'] = 100;
4014 $state['step_percent'] = 100;
4015 dbx()->set_remember_var('schema_batch', array(), 'dbxAdmin');
4016 return $this->render_process('Batch', $state, '', $backUrl);
4017 }
4018
4019 $current = $state['selects'][$pos];
4020 $oDD = dbx()->get_system_obj('dbxDD');
4021 $act = $state['act'] ?? '';
4022
4023 $parts = in_array($act, array('batch_db_to_dd', 'batch_backup_db', 'batch_restore_latest_db'), true)
4024 ? $this->decode_db_rid($current)
4025 : $this->selected_dd_parts($current);
4026
4027 if (($act == 'batch_dd_to_db' || $act == 'batch_dd_to_db_force') && count($parts) == 2) {
4028 $mode = ($act == 'batch_dd_to_db_force') ? 'force' : 'apply';
4029 $step = $oDD->sync_dd_to_db($parts[0], $parts[1], $mode);
4030 $state['step_percent'] = (int)($step['percent'] ?? 0);
4031
4032 if (($step['status'] ?? '') == 'finished') {
4033 $state['pos'] = $pos + 1;
4034 $state['step_percent'] = 100;
4035 } elseif (($step['status'] ?? '') == 'error') {
4036 $state['status'] = 'error';
4037 $state['message'] = $current . ': ' . ($step['message'] ?? 'error');
4038 } else {
4039 $state['message'] = $current . ': ' . ($step['message'] ?? 'running');
4040 }
4041 } elseif ($act == 'batch_db_to_dd' && count($parts) == 2 && $parts[0] && $parts[1]) {
4042 $ddIndex = $this->get_dd_index_by_db($this->get_dd_records());
4043 $dds = $this->get_dd_records_for_db($ddIndex, $parts[0], $parts[1]);
4044 $modul = $dds ? $dds[0]['modul'] : 'dbx';
4045 $dd = $dds ? $dds[0]['dd'] : $this->sanitize_dd_name($parts[1]);
4046 $step = $oDD->sync_db_to_dd($modul, $dd, 'merge', $parts[0], $parts[1]);
4047 $state['step_percent'] = (int)($step['percent'] ?? 0);
4048
4049 if (($step['status'] ?? '') == 'finished') {
4050 $state['pos'] = $pos + 1;
4051 $state['step_percent'] = 100;
4052 } elseif (($step['status'] ?? '') == 'error') {
4053 $state['status'] = 'error';
4054 $state['message'] = $current . ': ' . ($step['message'] ?? 'error');
4055 } else {
4056 $state['message'] = $current . ': ' . ($step['message'] ?? 'running');
4057 }
4058 } elseif ($act == 'batch_backup_db' && count($parts) == 2 && $parts[0] && $parts[1]) {
4059 $result = $this->backup_table($parts[0], $parts[1]);
4060 $state['step_percent'] = 100;
4061 if (!empty($result['ok'])) {
4062 $state['pos'] = $pos + 1;
4063 $state['message'] = $parts[0] . '|' . $parts[1] . ': Backup geschrieben';
4064 } else {
4065 $state['status'] = 'error';
4066 $state['message'] = $parts[0] . '|' . $parts[1] . ': ' . ($result['msg'] ?? 'backup error');
4067 }
4068 } elseif ($act == 'batch_restore_latest_db' && count($parts) == 2 && $parts[0] && $parts[1]) {
4069 $latest = $this->latest_backup($parts[0], $parts[1]);
4070 $file = (string)($latest['_file'] ?? '');
4071 $result = $file ? $this->restore_table_from_backup($file) : array('ok' => 0, 'msg' => 'Kein Backup gefunden');
4072 $state['step_percent'] = 100;
4073 if (!empty($result['ok'])) {
4074 $state['pos'] = $pos + 1;
4075 $state['message'] = $parts[0] . '|' . $parts[1] . ': Restore abgeschlossen';
4076 } else {
4077 $state['status'] = 'error';
4078 $state['message'] = $parts[0] . '|' . $parts[1] . ': ' . ($result['msg'] ?? 'restore error');
4079 }
4080 } elseif (in_array($act, array('batch_db_to_dd', 'batch_backup_db', 'batch_restore_latest_db'), true) && count($parts) == 2 && $parts[0] && !$parts[1]) {
4081 $state['pos'] = $pos + 1;
4082 $state['step_percent'] = 100;
4083 $state['message'] = $parts[0] . ': keine Tabelle, Schritt uebersprungen';
4084 } else {
4085 $state['status'] = 'error';
4086 $state['message'] = 'ungueltige Batch-Aktion';
4087 }
4088
4089 $done = min($total, (int)$state['pos']);
4090 $partial = (($state['status'] ?? '') == 'running') ? max(0, min(100, (int)($state['step_percent'] ?? 0))) / 100 : 0;
4091 $state['percent'] = (int)floor((min($total, $done + $partial) / max(1, $total)) * 100);
4092 $state['updated_at'] = date('Y-m-d H:i:s');
4093 dbx()->set_remember_var('schema_batch', $state, 'dbxAdmin');
4094
4095 $next = (($state['status'] ?? '') == 'running') ? $nextUrl : '';
4096 return $this->render_process('Batch', $state, $next, $backUrl);
4097 }
4098
4105 public function run($mode = '') {
4106 $run2 = dbx()->get_modul_var('dbx_run2', '', 'parameter');
4107 if (!$mode) {
4108 $mode = dbx()->get_modul_var('dbx_run1', 'dd', 'parameter');
4109 }
4110
4111 if ($run2 == 'sync_dd_to_db') {
4112 return $this->run_sync_dd_to_db();
4113 }
4114
4115 if ($run2 == 'sync_db_to_dd') {
4116 return $this->run_sync_db_to_dd();
4117 }
4118
4119 if ($run2 == 'transfer') {
4120 return $this->run_transfer();
4121 }
4122
4123 if ($run2 == 'backup_db_table') {
4124 return $this->run_backup_db_table();
4125 }
4126
4127 if ($run2 == 'restore_db_table') {
4128 return $this->run_restore_db_table();
4129 }
4130
4131 if ($run2 == 'mapping') {
4132 return $this->run_mapping_editor();
4133 }
4134
4135 if ($run2 == 'fields') {
4136 return $this->run_dd_fields_grid();
4137 }
4138
4139 if ($run2 == 'data_read') {
4140 return $this->run_data_read();
4141 }
4142
4143 if ($run2 == 'data_save') {
4144 return $this->run_data_save();
4145 }
4146
4147 if ($run2 == 'data_insert') {
4148 return $this->run_data_insert();
4149 }
4150
4151 if ($run2 == 'data_delete') {
4152 return $this->run_data_delete();
4153 }
4154
4155 if ($run2 == 'fields_read') {
4156 return $this->run_dd_fields_read();
4157 }
4158
4159 if ($run2 == 'fields_save') {
4160 return $this->run_dd_fields_save();
4161 }
4162
4163 if ($run2 == 'fields_insert') {
4164 return $this->run_dd_fields_insert();
4165 }
4166
4167 if ($run2 == 'fields_delete') {
4168 return $this->run_dd_fields_delete();
4169 }
4170
4171 if ($run2 == 'batch') {
4172 return $this->run_batch();
4173 }
4174
4175 if ($run2 == 'backup_db') {
4176 return $this->report_backup_db();
4177 }
4178
4179 if ($run2 == 'restore_db') {
4180 return $this->report_restore_db();
4181 }
4182
4183 if ($mode == 'db') {
4184 return $this->report_db();
4185 }
4186
4187 return $this->report_dd();
4188 }
4189}
4190?>
$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
schema_row_action_data($report, $data)
Erweitert Report-Zeilen um Daten fuer Row-Aktions-Templates.
run($mode='')
Zentraler Einstiegspunkt des dbxSchema-Moduls.
if((string)($formActionPolicy['action'] ?? '') !=='dbxAction.save'||!dbx() ->check_action_token((string)($formActionPolicy['scope'] ?? ''),(string)($formActionRoute['params']['dbx_token'] ?? ''))) $report
dbx()
Liefert die zentrale dbXapp-API als Singleton.
Definition dbxApi.php:2805
os_path(string $path)
Normalisiert einen Pfad fuer das aktuelle Betriebssystem.
Definition dbxApi.php:1538
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
if($resolved !==$expectedBase . 'files/test/') $config
if(!is_object( $db)) if($db->connect_db_server($server) !==1) $created
exit
Definition index.php:146
if( $demoId<=0) foreach(array('create_date', 'create_uid', 'update_date', 'update_uid', 'owner',) as $systemField) $items
Class dbxServer extends dbxObj
DBX schema administration.