dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxDB.class.php
Go to the documentation of this file.
1<?php
62class dbxDB {
63
64 public $db = array();
65
66 public $pdo = null;
67
68 public $_connected = 0;
69 public $_server = '';
70 public $_dbtype = '';
71
72 public $_insert_id = 0;
73
74 public $_update_count = 0;
75
76 public $_delete_count = 0;
77
78 public $_insert_count = 0;
79
80 public $_dbMessage = '';
81
82 public $oValidator = null;
85 public $_validation_error_flds = array();
86 public $_validation_warning_flds = array();
87
88 public $_validatior_rules = 0; // save insert update
89 public $_validatior_type = 1; // type der Daten prüfen
90 public $_validatior_error = 0; // Bei validate Fehler db error oder warning
91 public $_validatior_mode = 'clean'; // Bei validate Fehler daten 'clean' oder 'unset'
92
93 public $_fld_id;
94
95 public $_error = '';
96 public $_query = '';
97
99 public $_report_error = 1;
100
102 public $_error_status = '';
103
104 public $_error_text = '';
105
107 public int $_connect_timeout = 3;
108
109 private array $_reported_keys = [];
110
111 private array $_tx = [];
112
113 private int $_db_timer_depth = 0;
114
121 public function __construct() {
122 $this->oValidator = dbx()->get_system_obj('dbxValidator');
123 $this->db = array();
124 $this->_connected = 0;
125 $this->_server = '';
126 $this->_insert_id = 0;
127 $this->_update_count = 0;
128 $this->_delete_count = 0;
129 $this->_insert_count = 0;
130 $this->_dbMessage = '';
131 $this->_validation_error = 0;
132 $this->_validation_warning = 0;
133 $this->_validation_error_flds = array();
134 $this->_validation_warning_flds = array();
135 $this->_fld_id = 'id';
136 }
137
143 public function __destruct() {
144 $this->db = null;
145 }
146
152 public function clear_db_error(): void {
153 $this->_error_status = '';
154 $this->_error_text = '';
155 $this->_dbMessage = '';
156 }
157
163 public function get_error_status(): string {
164 return (string)$this->_error_status;
165 }
166
172 public function get_error_text(): string {
173 return (string)$this->_error_text;
174 }
175
181 public function get_insert_id(): int {
182 return (int)$this->_insert_id;
183 }
184
192 private function set_db_error(string $status, string $text): void {
193 $this->_error_status = $status;
194 $this->_error_text = $text;
195 $this->_dbMessage = $text;
196 $this->_error = $text;
197 }
198
209 private function report_db_error(string $status, string $rid, string $why, string $what, string $dedupKey = ''): void {
210 $message = trim($why);
211 if (trim($what) !== '') {
212 $message .= ($message !== '' ? ': ' : '') . trim($what);
213 }
214 $this->set_db_error($status, $message);
215
216 if (!(int)$this->_report_error) {
217 return;
218 }
219
220 if ($dedupKey !== '') {
221 if (isset($this->_reported_keys[$dedupKey])) {
222 return;
223 }
224 $this->_reported_keys[$dedupKey] = 1;
225 }
226
227 $sysMsgStatus = $status === 'access' ? 'security' : 'error';
228
229 try {
230 dbx()->sys_msg($sysMsgStatus, 'db', $rid, $why, $what);
231 } catch (Throwable $e) {
232 $fallback = sprintf(
233 'DBX database error [%s] %s (%s): %s; reporting failed: %s',
234 $status,
235 $why,
236 $rid,
237 $what,
238 $e->getMessage()
239 );
240 error_log($fallback);
241 } finally {
242 // sys_msg() schreibt ueber dasselbe dbxDB-Objekt. Dessen eigener
243 // erfolgreicher Insert darf den urspruenglichen DB-Fehler nicht
244 // ueberschreiben, sonst bleibt die eigentliche Ursache unsichtbar.
245 $this->set_db_error($status, $message);
246 }
247 }
248
252 private function connection_error_reason(Throwable $e): string {
253 $message = strtolower($e->getMessage());
254
255 if (str_contains($message, 'unknown database')
256 || str_contains($message, 'database does not exist')
257 || str_contains($message, 'does not exist')
258 ) {
259 return 'Datenbank nicht vorhanden';
260 }
261
262 if (str_contains($message, 'access denied')
263 || str_contains($message, 'authentication failed')
264 || str_contains($message, 'password authentication failed')
265 ) {
266 return 'Datenbank-Anmeldung fehlgeschlagen';
267 }
268
269 if (str_contains($message, 'connection refused')
270 || str_contains($message, 'server has gone away')
271 || str_contains($message, 'lost connection')
272 || str_contains($message, 'timed out')
273 || str_contains($message, 'timeout')
274 || str_contains($message, 'no connection could be made')
275 || str_contains($message, 'actively refused')
276 || str_contains($message, '2002')
277 || str_contains($message, '2003')
278 ) {
279 return 'Datenbankserver nicht erreichbar';
280 }
281
282 return 'Datenbankverbindung fehlgeschlagen';
283 }
284
290 private function is_missing_database_error(string $message): bool {
291 $message = strtolower($message);
292
293 return str_contains($message, 'unknown database')
294 || str_contains($message, 'database does not exist')
295 || str_contains($message, 'cannot open database')
296 || preg_match('/(?:sqlstate\‍[)?3d000/', $message) === 1
297 || preg_match('/(?:error|code)[^0-9]*1049/', $message) === 1;
298 }
299
320 function isSQLiteDatabaseLocked($databasePath) {
321 $isLocked = 0;
322
323 try {
324 $pdo = new PDO("sqlite:$databasePath");
325 $pdo->exec('PRAGMA locking_mode=NORMAL');
326 $pdo->beginTransaction();
327 } catch (PDOException $e) {
328 $isLocked = 1;
329 } finally {
330 if (isset($pdo) && $pdo->inTransaction()) {
331 $pdo->rollBack();
332 }
333 }
334
335 if ($isLocked) {
336 dbx()->debug("SQLITE db ($databasePath) Lock=($isLocked)");
337 }
338
339 return $isLocked;
340 }
341
366 function dbConnect($server, $dbType, $dbHost, $dbName = '', $dbUser = '', $dbPass = '', $dbPort = '') {
367 $ok = 1;
368 //dbx()->debug("#dbConnect Server=($server) Type=($dbType) Host=($dbHost) dbName=($dbName)", $this->db[$server] ?? null);
369
370 if (!isset($this->db[$server])) {
371 try {
372 switch ($dbType) {
373 case 'sqlite':
374 $dbName = dbx()->config_path_resolve($dbHost . $dbName);
375 $this->db[$server] = new PDO("sqlite:$dbName");
376 break;
377
378 case 'mysql':
379
380 $dsn = "mysql:host=$dbHost";
381 if ($dbPort !== '') {
382 $dsn .= ";port=$dbPort";
383 }
384 if ($dbName !== '') {
385 $dsn .= ";dbname=$dbName";
386 }
387 $dsn .= ";charset=utf8mb4";
388 $this->db[$server] = new PDO($dsn, $dbUser, $dbPass, [
389 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
390 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
391 PDO::ATTR_TIMEOUT => max(1, $this->_connect_timeout),
392 ]);
393
394 break;
395
396 case 'pgsql':
397 $this->db[$server] = new PDO("pgsql:host=$dbHost;dbname=$dbName", $dbUser, $dbPass);
398 break;
399
400 case 'sqlsrv':
401 $this->db[$server] = new PDO("sqlsrv:Server=$dbHost;Database=$dbName", $dbUser, $dbPass);
402 break;
403
404 case 'oci':
405 $dbtns = "(DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = //$dbHost)(PORT = $dbPort))
406 (CONNECT_DATA = (SERVICE_NAME = $dbName) ))";
407
408 $this->db[$server] = new PDO("oci:dbname=$dbtns;charset=utf8", $dbUser, $dbPass, [
409 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
410 PDO::ATTR_EMULATE_PREPARES => false,
411 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
412 ]);
413 break;
414
415 case 'firebird':
416 $this->db[$server] = new PDO("firebird:dbname=$dbHost:$dbName", $dbUser, $dbPass);
417 break;
418
419 case 'cubrid':
420 $this->db[$server] = new PDO("cubrid:host=$dbHost;dbname=$dbName", $dbUser, $dbPass);
421 break;
422
423 case 'dblib':
424 $this->db[$server] = new PDO("dblib:host=$dbHost;dbname=$dbName", $dbUser, $dbPass);
425 break;
426
427 case 'ibm':
428 $this->db[$server] = new PDO("ibm:DRIVER={IBM DB2 ODBC DRIVER};DATABASE=$dbName;HOSTNAME=$dbHost;PORT=$dbPort;PROTOCOL=TCPIP;UID=$dbUser;PWD=$dbPass;");
429 break;
430
431 case 'informix':
432 $this->db[$server] = new PDO("informix:host=$dbHost;service=$dbPort;database=$dbName;server=$server;protocol=onsoctcp;UID=$dbUser;PWD=$dbPass");
433 break;
434
435 case 'odbc':
436 $this->db[$server] = new PDO("odbc:$dbName", $dbUser, $dbPass);
437 break;
438
439 default:
440 throw new PDOException("Unsupported database type: $dbType");
441 }
442
443 $this->db[$server]->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
444 } catch (Throwable $e) {
445 $ok = 0;
446 $dbMessage = $e->getMessage();
447 unset($this->db[$server]);
448 $this->_query = "Connect Server ($server) Type=($dbType) Host=($dbHost) dbName=($dbName) dbUser=($dbUser) Port=($dbPort)";
449
450 $this->report_db_error(
451 'db',
452 (string)$server,
453 $this->connection_error_reason($e),
454 $dbMessage,
455 'db-connect|' . $server
456 );
457 }
458 }
459
460 return $ok;
461 }
462
463 private function quote_db_identifier_for_type(string $dbType, string $name): string {
464 $dbType = strtolower(trim($dbType));
465 if ($dbType === 'mysql') {
466 return '`' . str_replace('`', '``', $name) . '`';
467 }
468 if ($dbType === 'sqlsrv' || $dbType === 'dblib') {
469 return '[' . str_replace(']', ']]', $name) . ']';
470 }
471 return '"' . str_replace('"', '""', $name) . '"';
472 }
473
474 private function pdo_quote_value(PDO $pdo, string $value): string {
475 $quoted = $pdo->quote($value);
476 if ($quoted === false) {
477 return "'" . str_replace("'", "''", $value) . "'";
478 }
479 return $quoted;
480 }
481
482 public function can_connect_database_config(array $dbConfig, bool $withDatabase = false): int {
483 $dbType = strtolower(trim((string)($dbConfig['type'] ?? '')));
484 $dbName = trim((string)($dbConfig['dbname'] ?? ($dbConfig['name'] ?? '')));
485
486 if ($dbType === 'sqlite') {
487 $host = dbx()->config_path_resolve(rtrim((string)($dbConfig['host'] ?? ''), "/\\") . '/');
488 if ($withDatabase) {
489 $path = dbx()->config_path_resolve(rtrim((string)($dbConfig['host'] ?? ''), "/\\") . '/' . $dbName);
490 return ($dbName !== '' && is_file($path)) ? 1 : 0;
491 }
492 return is_dir($host) ? 1 : 0;
493 }
494
495 if ($dbType === '' || ($withDatabase && $dbName === '')) {
496 return 0;
497 }
498
499 $tmpServer = '__dbx_check_db_' . md5($dbType . $dbName . microtime(true));
500 if (isset($this->db[$tmpServer])) {
501 unset($this->db[$tmpServer]);
502 }
503
504 $ok = $this->dbConnect(
505 $tmpServer,
506 $dbType,
507 $dbConfig['host'] ?? '',
508 $withDatabase ? $dbName : '',
509 $dbConfig['user'] ?? '',
510 $dbConfig['pass'] ?? '',
511 $dbConfig['port'] ?? ''
512 );
513
514 if (isset($this->db[$tmpServer])) {
515 unset($this->db[$tmpServer]);
516 }
517
518 return $ok ? 1 : 0;
519 }
520
521 public function ensure_database_exists(string $server, array $dbConfig): int {
522 $dbType = strtolower(trim((string)($dbConfig['type'] ?? '')));
523 $dbName = (string)($dbConfig['dbname'] ?? ($dbConfig['name'] ?? ''));
524 $dbName = trim($dbName);
525
526 if ($dbName === '') {
527 return 0;
528 }
529
530 if ($dbType === 'sqlite') {
531 $host = dbx()->config_path_resolve(rtrim((string)($dbConfig['host'] ?? ''), "/\\") . '/');
532 if ($host !== '' && !is_dir($host)) {
533 @mkdir($host, 0777, true);
534 }
535 return is_dir($host) ? 1 : 0;
536 }
537
538 $adminDb = '';
539 switch ($dbType) {
540 case 'mysql':
541 $adminDb = '';
542 break;
543 case 'pgsql':
544 $adminDb = (string)($dbConfig['admin_dbname'] ?? $dbConfig['maintenance_db'] ?? 'postgres');
545 break;
546 case 'sqlsrv':
547 case 'dblib':
548 $adminDb = (string)($dbConfig['admin_dbname'] ?? $dbConfig['maintenance_db'] ?? 'master');
549 break;
550 default:
551 return 0;
552 }
553
554 $tmpServer = '__dbx_create_db_' . md5($server . $dbType . microtime(true));
555 if (isset($this->db[$tmpServer])) {
556 unset($this->db[$tmpServer]);
557 }
558
559 $ok = $this->dbConnect(
560 $tmpServer,
561 $dbType,
562 $dbConfig['host'] ?? '',
563 $adminDb,
564 $dbConfig['user'] ?? '',
565 $dbConfig['pass'] ?? '',
566 $dbConfig['port'] ?? ''
567 );
568
569 if (!$ok || !isset($this->db[$tmpServer])) {
570 if (isset($this->db[$tmpServer])) {
571 unset($this->db[$tmpServer]);
572 }
573 return 0;
574 }
575
576 try {
577 $pdo = $this->db[$tmpServer];
578 switch ($dbType) {
579 case 'mysql':
580 $sql = 'CREATE DATABASE IF NOT EXISTS ' . $this->quote_db_identifier_for_type($dbType, $dbName)
581 . ' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
582 $pdo->exec($sql);
583 break;
584
585 case 'pgsql':
586 $exists = $pdo->query('SELECT 1 FROM pg_database WHERE datname = ' . $this->pdo_quote_value($pdo, $dbName));
587 if (!$exists || !$exists->fetchColumn()) {
588 $pdo->exec('CREATE DATABASE ' . $this->quote_db_identifier_for_type($dbType, $dbName) . " ENCODING 'UTF8'");
589 }
590 break;
591
592 case 'sqlsrv':
593 case 'dblib':
594 $literal = $this->pdo_quote_value($pdo, $dbName);
595 $sql = 'IF DB_ID(N' . $literal . ') IS NULL CREATE DATABASE ' . $this->quote_db_identifier_for_type($dbType, $dbName);
596 $pdo->exec($sql);
597 break;
598 }
599
600 unset($this->db[$tmpServer]);
601 dbx()->debug("database ensured Server=($server) Type=($dbType) DB=($dbName)");
602 return 1;
603 } catch (PDOException $e) {
604 $this->_dbMessage = $e->getMessage();
605 $this->report_db_error('db', (string)$server, 'Datenbank nicht vorhanden', $e->getMessage(), 'db-create|' . $server);
606 unset($this->db[$tmpServer]);
607 return 0;
608 }
609 }
610
643 public function connect_db_server(string $server): int {
644
645 $ok = 0;
646 $this->_server = 'try:' . $server;
647 $this->_connected = 0;
648 $this->_dbtype = '';
649
650 // Eine bereits geoeffnete dynamische Modul-DB besitzt absichtlich
651 // keinen dauerhaften Eintrag in config.php. Der bisherige erneute
652 // Config-Lookup konnte deshalb beim zweiten Aufruf fehlschlagen,
653 // obwohl die PDO-Verbindung weiterhin gueltig war.
654 if (isset($this->db[$server]) && $this->db[$server] instanceof PDO) {
655 $this->clear_db_error();
656 $this->_error = '';
657 $this->_connected = 1;
658 $this->_server = $server;
659 $this->_dbtype = $this->get_db_type($server);
660 return 1;
661 }
662
663 $config = [];
664
665 if (!isset($this->db[$server])) {
666 $sqlite_modul = '';
667 $sqlite_name = '';
668
669 if (preg_match('/\.(db3|sqlite|sqlite3)$/i', $server)) {
670 $activ_modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
671
672 if (strpos($server, '|') !== false) {
673 $parts = explode('|', $server, 2);
674 $sqlite_modul = trim($parts[0]);
675 $sqlite_name = trim($parts[1]);
676
677 if ($sqlite_modul === 'modul' || $sqlite_modul === '') {
678 $sqlite_modul = $activ_modul;
679 }
680 } else {
681 $sqlite_modul = $activ_modul;
682 $sqlite_name = $server;
683 }
684
685 $file = '';
686
687 $file1 = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/dbx/db/' . $sqlite_name);
688 $file2 = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/' . $sqlite_modul . '/db/' . $sqlite_name);
689
690 if (file_exists($file2)) {
691 $file = $file2;
692 } elseif (file_exists($file1)) {
693 $file = $file1;
694 $sqlite_modul = 'dbx';
695 } else {
696 $missingFile = $file2;
697 $this->report_db_error(
698 'db',
699 $server,
700 'Datenbank nicht vorhanden',
702 'db-missing|' . $server
703 );
704 return 0;
705 }
706
707 if ($file) {
708 $dir = dirname($file);
709 if (!is_dir($dir)) {
710 @mkdir($dir, 0777, true);
711 }
712 $config['db'][$server] = [
713 'type' => 'sqlite',
714 'host' => dbx()->config_path_store($dir . '/', true),
715 'dbname' => basename($file),
716 'user' => '',
717 'pass' => '',
718 'port' => ''
719 ];
720
721 //dbx()->debug("sqlite server resolved Server=($server) Modul=($sqlite_modul) Name=($sqlite_name) File=($file)");
722 }
723 }
724 }
725
726 if (!isset($config['db'][$server])) {
727 //dbx()->debug("read cfg dbx for db");
728 $config = dbx()->get_config('dbx');
729 }
730
731 if (!isset($config['db'][$server])) {
732 dbx()->debug("## no config ERROR connect_db_server Server=($server)", $config);
733 $this->report_db_error(
734 'db',
735 $server,
736 'Datenbank nicht vorhanden',
737 'Keine Server-Konfiguration',
738 'db-config|' . $server
739 );
740 } else {
741 $dbConfig = $config['db'][$server];
742
743 if (!$this->db_server_config_is_active($server, $dbConfig)) {
744 $this->_dbMessage = 'Datenbankserver deaktiviert';
745 $this->_error = $this->_dbMessage;
746 return 0;
747 }
748
749 $_SESSION['dbx']['config']['dbx']['db'][$server] = $dbConfig;
750
751 $dbName = $dbConfig['dbname'] ?? ($dbConfig['name'] ?? '');
752
753
754 $ok = $this->dbConnect(
755 $server,
756 $dbConfig['type'] ?? 'sqlite',
757 $dbConfig['host'] ?? '',
758 $dbName,
759 $dbConfig['user'] ?? '',
760 $dbConfig['pass'] ?? '',
761 $dbConfig['port'] ?? ''
762 );
763
764 if (!$ok
765 && trim((string)$dbName) !== ''
766 && $this->is_missing_database_error((string)$this->_dbMessage)
767 ) {
768 $firstMessage = $this->_dbMessage;
769 if ($this->ensure_database_exists($server, $dbConfig)) {
770 if (isset($this->db[$server])) {
771 unset($this->db[$server]);
772 }
773 $this->_dbMessage = '';
774 $this->_error = '';
775 $ok = $this->dbConnect(
776 $server,
777 $dbConfig['type'] ?? 'mysql',
778 $dbConfig['host'] ?? '',
779 $dbName,
780 $dbConfig['user'] ?? '',
781 $dbConfig['pass'] ?? '',
782 $dbConfig['port'] ?? ''
783 );
784 } elseif ($this->_dbMessage === '' && $firstMessage !== '') {
785 $this->_dbMessage = $firstMessage;
786 }
787 }
788 }
789
790 if ($ok) {
791 //dbx()->debug("connect $server = ok");
792
793 $this->clear_db_error();
794 $this->_error = '';
795 $dbType = $this->get_db_type($server);
796 $this->_connected = 1;
797 $this->_server = $server;
798 $this->_dbtype = $dbType;
799 }
800
801 return $ok;
802 }
803
810 public function db_server_config_is_active(string $server, array $dbConfig): bool {
811 $type = strtolower(trim((string)($dbConfig['type'] ?? '')));
812 $name = trim((string)($dbConfig['dbname'] ?? ($dbConfig['name'] ?? '')));
813
814 if ($type === 'sqlite' || $type === 'sqlite3' || preg_match('/\.(db3|sqlite|sqlite3)$/i', $server . ' ' . $name)) {
815 return true;
816 }
817
818 if (!array_key_exists('activ', $dbConfig)) {
819 return true;
820 }
821
822 $active = strtolower(trim((string)$dbConfig['activ']));
823 return !in_array($active, array('', '0', 'false', 'no', 'nein', 'off', 'deaktiv', 'inactive', 'disabled'), true);
824 }
825
840 public function get_db_type($server) {
841 $dbType = 'sqlite';
842 $config = dbx()->get_config('dbx');
843
844 if (isset($config['db'][$server]['type'])) {
845 $dbType = $config['db'][$server]['type'];
846 } elseif (preg_match('/\.(db3|sqlite|sqlite3)$/i', (string) $server)) {
847 $dbType = 'sqlite';
848 }
849
850 return $dbType;
851 }
852
862 protected function get_dd_server_bindings(): array {
863 $config = dbx()->get_config('dbx');
864 $bindings = $config['dd_server_bindings'] ?? array();
865
866 return is_array($bindings) ? $bindings : array();
867 }
868
874 private function find_dd_server_binding(
875 array $bindings,
876 string $ddModule,
877 string $ddName
878 ): array {
879 $exact = $ddModule . '|' . $ddName;
880 $candidates = array($exact, $ddName);
881
882 foreach ($candidates as $candidate) {
883 if (array_key_exists($candidate, $bindings)) {
884 return array(
885 'key' => $candidate,
886 'server' => trim((string)$bindings[$candidate]),
887 );
888 }
889 }
890
891 $lowerCandidates = array_map('strtolower', $candidates);
892 foreach ($bindings as $key => $server) {
893 $position = array_search(strtolower(trim((string)$key)), $lowerCandidates, true);
894 if ($position !== false) {
895 return array(
896 'key' => (string)$key,
897 'server' => trim((string)$server),
898 );
899 }
900 }
901
902 return array('key' => '', 'server' => '');
903 }
904
913 protected function is_valid_dd_server_binding(string $server): bool {
914 $server = trim($server);
915 if ($server === '') {
916 return false;
917 }
918
919 if (preg_match('/\.(db3|sqlite|sqlite3)$/i', $server)) {
920 $parts = strpos($server, '|') !== false
921 ? explode('|', $server, 2)
922 : array('', $server);
923 $module = trim((string)($parts[0] ?? ''));
924 $file = trim((string)($parts[1] ?? ''));
925
926 return ($module === '' || preg_match('/^[A-Za-z0-9_]+$/', $module) === 1)
927 && basename($file) === $file
928 && preg_match('/^[A-Za-z0-9_.-]+\.(db3|sqlite|sqlite3)$/i', $file) === 1;
929 }
930
931 $config = dbx()->get_config('dbx');
932 $dbConfig = $config['db'][$server] ?? null;
933
934 return is_array($dbConfig)
935 && $this->db_server_config_is_active($server, $dbConfig);
936 }
937
953 public function get_dd_server_binding_info(string $dd): array {
954 $dd_sys = $this->load_dd($dd);
955 $dd_status = $dd_sys['dd_status'] ?? 0;
956 $dd_modul = (string)($dd_sys['dd_modul'] ?? '');
957 $dd_name = (string)($dd_sys['dd_name'] ?? '');
958
959 if ($dd_status != 1) {
960 return array(
961 'dd' => $dd,
962 'binding_key' => '',
963 'declared_server' => '',
964 'resolved_server' => '',
965 'source' => 'missing-dd',
966 'valid' => false,
967 );
968 }
969
970 $cache = $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name] ?? array();
971 $declared = trim((string)(
972 $cache['declared_server']
973 ?? ($cache['table']['server'] ?? 'default')
974 ));
975 $binding = $this->find_dd_server_binding(
976 $this->get_dd_server_bindings(),
977 $dd_modul,
978 $dd_name
979 );
980 $resolved = $binding['key'] !== '' ? $binding['server'] : $declared;
981 $valid = $binding['key'] === ''
982 ? $resolved !== ''
983 : $this->is_valid_dd_server_binding($resolved);
984
985 if (!$valid && $binding['key'] !== '') {
986 dbx()->sys_msg(
987 'error',
988 'db',
989 $dd_modul . '|' . $dd_name,
990 'ungueltige lokale DD-Serverbindung',
991 $binding['key'] . ' => ' . $resolved
992 );
993 $resolved = '';
994 }
995
996 return array(
997 'dd' => $dd_modul . '|' . $dd_name,
998 'binding_key' => $binding['key'],
999 'declared_server' => $declared,
1000 'resolved_server' => $resolved,
1001 'source' => $binding['key'] !== '' ? 'local-binding' : 'dd-default',
1002 'valid' => $valid,
1003 );
1004 }
1005
1015 public function get_dd_server(string $dd): string {
1017 return (string)($binding['resolved_server'] ?? '');
1018 }
1019
1030 public function get_csv_seperator($dd) {
1031 $csv_seperator = ';';
1032
1033 $dd_sys = $this->load_dd($dd);
1034 $dd_status = $dd_sys['dd_status'] ?? 0;
1035 $dd_modul = $dd_sys['dd_modul'] ?? '';
1036 $dd_name = $dd_sys['dd_name'] ?? '';
1037
1038 if ($dd_status == 1) {
1039 $csv_seperator = $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['table']['csv'] ?? ';';
1040 }
1041
1042 return $csv_seperator;
1043 }
1044
1055 public function get_dd_primary(string $dd): string {
1056 $dd_sys = $this->load_dd($dd);
1057 $dd_status = $dd_sys['dd_status'] ?? 0;
1058 $dd_modul = $dd_sys['dd_modul'] ?? '';
1059 $dd_name = $dd_sys['dd_name'] ?? '';
1060
1061 if ($dd_status == 1) {
1062 $primary = $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['table']['primary'] ?? '';
1063
1064 if ($primary !== '') {
1065 return $primary;
1066 }
1067 }
1068
1069 return 'id';
1070 }
1071
1079 public function get_dd_sort_flds($dd) {
1080 $fld = '';
1081 return $fld;
1082 }
1083
1091 public function get_dd_sort_desc($dd) {
1092 $desc = 'ASC';
1093 return $desc;
1094 }
1095
1121 public function add_db_fld($server, $table, $field) {
1122 $ok = $this->connect_db_server($server);
1123
1124 if (!$ok) {
1125 return false;
1126 }
1127
1128 $typeMap = [
1129 'int' => ['mysql' => 'INT', 'sqlite' => 'INTEGER', 'pgsql' => 'INTEGER', 'sqlsrv' => 'INT', 'oci' => 'NUMBER', 'firebird' => 'INTEGER', 'cubrid' => 'INTEGER', 'dblib' => 'INT', 'ibm' => 'INTEGER', 'informix' => 'INTEGER', 'odbc' => 'INTEGER'],
1130 'varchar' => ['mysql' => 'VARCHAR', 'sqlite' => 'TEXT', 'pgsql' => 'VARCHAR', 'sqlsrv' => 'VARCHAR', 'oci' => 'VARCHAR2', 'firebird' => 'VARCHAR', 'cubrid' => 'VARCHAR', 'dblib' => 'VARCHAR', 'ibm' => 'VARCHAR', 'informix' => 'VARCHAR', 'odbc' => 'VARCHAR'],
1131 'text' => ['mysql' => 'TEXT', 'sqlite' => 'TEXT', 'pgsql' => 'TEXT', 'sqlsrv' => 'TEXT', 'oci' => 'CLOB', 'firebird' => 'BLOB', 'cubrid' => 'STRING', 'dblib' => 'TEXT', 'ibm' => 'CLOB', 'informix' => 'TEXT', 'odbc' => 'LONGVARCHAR'],
1132 'bool' => ['mysql' => 'TINYINT(1)', 'sqlite' => 'INTEGER', 'pgsql' => 'BOOLEAN', 'sqlsrv' => 'BIT', 'oci' => 'NUMBER(1)', 'firebird' => 'SMALLINT', 'cubrid' => 'SMALLINT', 'dblib' => 'BIT', 'ibm' => 'SMALLINT', 'informix' => 'BOOLEAN', 'odbc' => 'BOOLEAN'],
1133 'date' => ['mysql' => 'DATE', 'sqlite' => 'TEXT', 'pgsql' => 'DATE', 'sqlsrv' => 'DATE', 'oci' => 'DATE', 'firebird' => 'DATE', 'cubrid' => 'DATE', 'dblib' => 'DATE', 'ibm' => 'DATE', 'informix' => 'DATE', 'odbc' => 'DATE']
1134 ];
1135
1136 $dbType = $this->get_db_type($server);
1137 $type = $typeMap[$field['type']][$dbType] ?? $field['type'];
1138
1139 $sql = "ALTER TABLE $table ADD COLUMN {$field['name']} $type";
1140
1141 if (!empty($field['length']) && in_array($field['type'], ['int', 'varchar'])) {
1142 $sql .= "({$field['length']})";
1143 }
1144
1145 if (isset($field['default']) && ($field['default'] !== '' || $field['default'] === 0)) {
1146 $sql .= " DEFAULT '{$field['default']}'";
1147 }
1148
1149 dbx()->debug("ADD-FLD SQL=($sql)");
1150
1151 if (($field['index'] ?? '') === 'PRI') {
1152 $sqlIndex = "ALTER TABLE $table ADD PRIMARY KEY ({$field['name']})";
1153 dbx()->debug("ADD-PRIMARY-KEY SQL=($sqlIndex)");
1154 } elseif (($field['index'] ?? '') === 'MU') {
1155 $sqlIndex = "CREATE INDEX idx_{$field['name']} ON $table ({$field['name']})";
1156 dbx()->debug("ADD-INDEX SQL=($sqlIndex)");
1157 }
1158
1159 return $ok;
1160 }
1161
1176 function get_dd_table($dd, $rec = 0) {
1177 $dd_table = 0;
1178
1179 $dd_sys = $this->load_dd($dd);
1180 $dd_status = $dd_sys['dd_status'] ?? 0;
1181 $dd_modul = $dd_sys['dd_modul'] ?? '';
1182 $dd_name = $dd_sys['dd_name'] ?? '';
1183
1184 if ($dd_status == 1 && !$rec) {
1185 $dd_table = $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['table']['table'] ?? 0;
1186 }
1187
1188 if ($dd_status == 1 && $rec) {
1189 $dd_table = $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['table'] ?? 0;
1190 if (is_array($dd_table)) {
1191 $dd_table['server'] = $this->get_dd_server((string)$dd);
1192 }
1193 }
1194
1195 return $dd_table;
1196 }
1197
1209 function get_dd_autosync($dd, $rec = 0) {
1210 $dd_sync = 0;
1211
1212 $dd_sys = $this->load_dd($dd);
1213 $dd_status = $dd_sys['dd_status'] ?? 0;
1214 $dd_modul = $dd_sys['dd_modul'] ?? '';
1215 $dd_name = $dd_sys['dd_name'] ?? '';
1216
1217 if ($dd_status == 1 && !$rec) {
1218 $dd_sync = $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['table']['autosync'] ?? 0;
1219 }
1220
1221 if ($dd_status == 1 && $rec) {
1222 $dd_sync = $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['autosync'] ?? 0;
1223 }
1224
1225 return $dd_sync;
1226 }
1227
1267 public function load_dd(string $dd): array {
1268 $dd_sys = array();
1269
1270 $activ_modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
1271 $is_explicit_modul = false;
1272 $active_language = function_exists('dbx_lng_current')
1273 ? strtolower(trim((string) dbx_lng_current()))
1274 : strtolower(trim((string) dbx()->get_system_var('dbx_lng', 'de')));
1275
1276 if ($active_language === '' || !preg_match('/^[a-z]{2,3}$/', $active_language)) {
1277 $active_language = 'de';
1278 }
1279
1280 $cache_matches_language = static function (array $cache, string $language): bool {
1281 $table = isset($cache['table']) && is_array($cache['table']) ? $cache['table'] : array();
1282 $dynamic = !empty($cache['language_dynamic']) || (($table['language'] ?? '') === '*');
1283
1284 if (!$dynamic) {
1285 return true;
1286 }
1287
1288 return strtolower(trim((string) ($table['language'] ?? ''))) === $language;
1289 };
1290
1291 if (strpos($dd, '|') !== false) {
1292 $parts = explode('|', $dd, 2);
1293 $dd_modul = trim($parts[0]);
1294 $dd_name = trim($parts[1]);
1295 $is_explicit_modul = true;
1296
1297 if ($dd_modul === 'modul' || $dd_modul === '') {
1298 $dd_modul = $activ_modul;
1299 }
1300 } else {
1301 $dd_modul = $activ_modul;
1302 $dd_name = $dd;
1303 }
1304
1305 if (isset($_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name])) {
1306 $cached_dd = $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name];
1307 if ($cache_matches_language($cached_dd, $active_language)) {
1308 $dd_sys['dd_status'] = 1;
1309 $dd_sys['dd_modul'] = $dd_modul;
1310 $dd_sys['dd_name'] = $dd_name;
1311
1312 return $dd_sys;
1313 }
1314
1315 unset($_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]);
1316 }
1317
1318 if (!$is_explicit_modul && isset($_SESSION['dbx']['cache']['dd']['dbx'][$dd_name])) {
1319 $cached_dd = $_SESSION['dbx']['cache']['dd']['dbx'][$dd_name];
1320 if ($cache_matches_language($cached_dd, $active_language)) {
1321 $dd_sys['dd_status'] = 1;
1322 $dd_sys['dd_modul'] = 'dbx';
1323 $dd_sys['dd_name'] = $dd_name;
1324
1325 return $dd_sys;
1326 }
1327
1328 unset($_SESSION['dbx']['cache']['dd']['dbx'][$dd_name]);
1329 }
1330
1331 $dd_file = '';
1332
1333 $dd_file1 = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/dbx/dd/' . $dd_name . '.dd.php');
1334 $dd_file2 = dbx()->os_path(dbx()->get_base_dir() . "dbx/modules/$dd_modul/dd/" . $dd_name . '.dd.php');
1335
1336 if (file_exists($dd_file2)) {
1337 $dd_file = $dd_file2;
1338 } elseif (file_exists($dd_file1)) {
1339 $dd_file = $dd_file1;
1340 $dd_modul = 'dbx';
1341 }
1342
1343 $dd_sys['dd_modul'] = $dd_modul;
1344 $dd_sys['dd_name'] = $dd_name;
1345
1346 dbx()->debug("##dbxDB-load_dd=($dd) modul=($dd_modul) dd=($dd_name) file=($dd_file) aktiv modul=($activ_modul)");
1347
1348 if (empty($dd_file)) {
1349 dbx()->sys_msg('error', 'dd', $dd, 'missing', 'No dd Path');
1350 $dd_sys['dd_status'] = 0;
1351 return $dd_sys;
1352 }
1353
1354 include $dd_file;
1355
1356 if (!isset($table) || !is_array($table)) {
1357 $dd_sys['dd_status'] = -1;
1358 return $dd_sys;
1359 }
1360
1361 /*
1362 * Sprachabhaengige DDs kennzeichnen die neutrale Definition mit
1363 * language="*". In diesem Fall muss der Loader selbst die DD der
1364 * aktiven Sprache verwenden. Andernfalls wuerde z. B.
1365 * dbxContentFolder auf die nicht vorhandene Tabelle content_folder
1366 * statt auf content_folder_de zeigen.
1367 */
1368 $language_dynamic = (($table['language'] ?? '') === '*');
1369 if ($language_dynamic) {
1370 $language = $active_language;
1371 $table_base = trim((string) ($table['table'] ?? ''));
1372 if ($table_base !== '') {
1373 $language_dd_name = $table_base . '_' . $language;
1374 $language_dd_file1 = dbx()->os_path(
1375 dbx()->get_base_dir() . 'dbx/modules/dbx/dd/' . $language_dd_name . '.dd.php'
1376 );
1377 $language_dd_file2 = dbx()->os_path(
1378 dbx()->get_base_dir() . 'dbx/modules/' . $dd_modul . '/dd/' . $language_dd_name . '.dd.php'
1379 );
1380 $language_dd_file = '';
1381
1382 if (file_exists($language_dd_file2)) {
1383 $language_dd_file = $language_dd_file2;
1384 } elseif (file_exists($language_dd_file1)) {
1385 $language_dd_file = $language_dd_file1;
1386 }
1387
1388 if ($language_dd_file !== '') {
1389 unset($table, $fields, $indexes);
1390 include $language_dd_file;
1391 $dd_file = $language_dd_file;
1392 } else {
1393 $table['table'] = $language_dd_name;
1394 $table['datadic'] = $language_dd_name;
1395 $table['language'] = $language;
1396 }
1397 }
1398 }
1399
1400 dbx()->register_editor_file('dd', $dd_file);
1401
1402 if (!isset($fields) || !is_array($fields)) {
1403 $fields = array();
1404 }
1405
1406 if (!isset($indexes) || !is_array($indexes)) {
1407 $indexes = array();
1408 }
1409
1410 if (isset($table['server'])
1411 && is_string($table['server'])
1412 && strpos($table['server'], '|') === false
1413 && preg_match('/\.(db3|sqlite|sqlite3)$/i', $table['server'])
1414 ) {
1415 $sqlite_file = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/' . $dd_modul . '/db/' . $table['server']);
1416
1417 if ($dd_modul !== '' && file_exists($sqlite_file)) {
1418 $table['server'] = $dd_modul . '|' . $table['server'];
1419 }
1420 }
1421
1422 $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name] = [
1423 'table' => $table,
1424 'fields' => $fields,
1425 'indexes' => $indexes,
1426 'file' => $this->normalize_editor_file_path($dd_file),
1427 'language_dynamic' => $language_dynamic,
1428 'declared_server' => (string)($table['server'] ?? 'default'),
1429 ];
1430
1431 //dbx()->debug("#session set dd ($dd) Modul=($dd_modul) Name=($dd_name)");
1432
1433 $dd_sys['dd_status'] = 1;
1434 $dd_sys['dd_modul'] = $dd_modul;
1435 $dd_sys['dd_name'] = $dd_name;
1436
1437 return $dd_sys;
1438 }
1439
1446 public function get_dd_file(string $dd): string {
1447 $dd_sys = $this->load_dd($dd);
1448 $dd_status = $dd_sys['dd_status'] ?? 0;
1449 $dd_modul = $dd_sys['dd_modul'] ?? '';
1450 $dd_name = $dd_sys['dd_name'] ?? '';
1451
1452 if ($dd_status != 1 || $dd_modul === '' || $dd_name === '') {
1453 return '';
1454 }
1455
1456 return $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['file'] ?? '';
1457 }
1458
1465 private function normalize_editor_file_path(string $file): string {
1466 return dbx()->editor_file_path($file);
1467 }
1468
1481 public function query(string $server, string $sql): PDOStatement|int {
1482 if (!$sql) {
1483 return 0;
1484 }
1485
1486 if (!$this->connect_db_server($server)) {
1487 return 0;
1488 }
1489
1490 $maxRetry = 5;
1491
1492 for ($try = 0; $try < $maxRetry; $try++) {
1493 try {
1494 $stmt = $this->db[$server]->prepare($sql);
1495 $stmt->execute();
1496
1497 return $stmt;
1498 } catch (PDOException $e) {
1499 if (empty($this->_tx[$server]) && $this->isRetryable($e) && $try < $maxRetry - 1) {
1500 usleep(120000 + random_int(0, 150000));
1501 continue;
1502 }
1503
1504 $this->report_db_error(
1505 'sql',
1506 $server,
1507 'SQL-Fehler',
1508 $e->getMessage(),
1509 'sql|' . $server . '|' . md5($sql)
1510 );
1511
1512 return 0;
1513 }
1514 }
1515
1516 return 0;
1517 }
1518
1533 private function db_timer_dd_section(string $type, string $dd): string {
1534 $dd = trim($dd);
1535
1536 if ($dd === '') {
1537 return '';
1538 }
1539
1540 $prefix = $type === 'save' ? 'db-save-' : 'db-select-';
1541 $safe = preg_replace('/[^A-Za-z0-9]+/', '-', $dd);
1542 $safe = trim((string) $safe, '-');
1543
1544 if ($safe === '') {
1545 return '';
1546 }
1547
1548 $max = 80 - strlen($prefix);
1549 if (strlen($safe) > $max) {
1550 $hash = substr(md5($dd), 0, 6);
1551 $safe = rtrim(substr($safe, 0, max(1, $max - 7)), '-') . '-' . $hash;
1552 }
1553
1554 return $prefix . $safe;
1555 }
1556
1583 private function db_timers_start(string $type, string $dd = ''): array {
1584 if ((int) dbx()->get_system_var('dbx_performance_timer_store', 0, 'int') === 1) {
1585 return array();
1586 }
1587
1588 if ($this->_db_timer_depth > 0) {
1589 return array();
1590 }
1591
1592 $this->_db_timer_depth++;
1593
1594 $type = $type === 'save' ? 'save' : 'select';
1595 $base = $type === 'save' ? 'db-save' : 'db-select';
1596 $sections = array(
1597 'db-total' => 'db total',
1598 $base => $type,
1599 );
1600
1601 $ddSection = $this->db_timer_dd_section($type, $dd);
1602 if ($ddSection !== '') {
1603 $sections[$ddSection] = $type . ' ' . $dd;
1604 }
1605
1606 foreach ($sections as $section => $info) {
1607 dbx()->timer($section, $info);
1608 }
1609
1610 return array_keys($sections);
1611 }
1612
1622 private function db_timers_stop(array $sections): void {
1623 if (!$sections) {
1624 return;
1625 }
1626
1627 for ($i = count($sections) - 1; $i >= 0; $i--) {
1628 dbx()->timer($sections[$i]);
1629 }
1630
1631 $this->_db_timer_depth = max(0, $this->_db_timer_depth - 1);
1632 }
1633
1641 public function begin(string $dd): int {
1642 $server = $this->get_dd_server($dd);
1643
1644 if (!$server) {
1645 return 0;
1646 }
1647
1648 return $this->beginServer($server);
1649 }
1650
1658 public function rollback(string $dd): int {
1659 $server = $this->get_dd_server($dd);
1660
1661 if (!$server) {
1662 return 0;
1663 }
1664
1665 return $this->rollbackServer($server);
1666 }
1667
1685 private function beginServer(string $server): int {
1686 if (!$this->connect_db_server($server)) {
1687 return 0;
1688 }
1689
1690 if (!empty($this->_tx[$server])) {
1691 return 1;
1692 }
1693
1694 $type = $this->get_db_type($server);
1695 $pdo = $this->db[$server];
1696
1697 try {
1698 switch ($type) {
1699 case 'sqlite':
1700 $pdo->exec('BEGIN IMMEDIATE TRANSACTION');
1701 break;
1702
1703 case 'mysql':
1704 $pdo->beginTransaction();
1705 break;
1706
1707 default:
1708 $pdo->beginTransaction();
1709 }
1710
1711 $this->_tx[$server] = true;
1712 return 1;
1713 } catch (PDOException $e) {
1714 $this->report_db_error('sql', $server, 'SQL-Fehler', $e->getMessage(), 'sql-tx-begin|' . $server);
1715 return 0;
1716 }
1717 }
1718
1730 private function rollbackServer(string $server): int {
1731 if (!$this->connect_db_server($server)) {
1732 return 0;
1733 }
1734
1735 if (empty($this->_tx[$server])) {
1736 return 1;
1737 }
1738
1739 $type = $this->get_db_type($server);
1740 $pdo = $this->db[$server];
1741
1742 try {
1743 if ($type === 'sqlite') {
1744 $pdo->exec('ROLLBACK');
1745 } else {
1746 $pdo->rollBack();
1747 }
1748
1749 unset($this->_tx[$server]);
1750 return 1;
1751 } catch (PDOException $e) {
1752 $this->report_db_error('sql', $server, 'SQL-Fehler', $e->getMessage(), 'sql-tx-rollback|' . $server);
1753
1754 unset($this->_tx[$server]);
1755 return 0;
1756 }
1757 }
1758
1770 public function commit($dd) {
1771 $server = $this->get_dd_server($dd);
1772 dbx()->debug("commit($server)");
1773
1774 if (!$this->connect_db_server($server)) {
1775 return 0;
1776 }
1777
1778 if (empty($this->_tx[$server])) {
1779 return 1;
1780 }
1781
1782 $type = $this->get_db_type($server);
1783 $pdo = $this->db[$server];
1784
1785 try {
1786 if ($type === 'sqlite') {
1787 $pdo->exec('COMMIT');
1788 } else {
1789 $pdo->commit();
1790 }
1791
1792 unset($this->_tx[$server]);
1793 return 1;
1794 } catch (PDOException $e) {
1795 $this->report_db_error('sql', $server, 'SQL-Fehler', $e->getMessage(), 'sql-tx-commit|' . $server);
1796
1797 unset($this->_tx[$server]);
1798 return 0;
1799 }
1800 }
1801
1809 private function isRetryable(PDOException $e): bool {
1810 $msg = strtolower($e->getMessage());
1811
1812 return
1813 str_contains($msg, 'deadlock') ||
1814 str_contains($msg, 'locked') ||
1815 str_contains($msg, 'lock wait timeout') ||
1816 str_contains($msg, 'database is locked') ||
1817 str_contains($msg, 'busy');
1818 }
1819
1829 public function exec(string $server, string $sql): int {
1830 if (!$this->connect_db_server($server)) {
1831 return 0;
1832 }
1833
1834 $maxRetry = 5;
1835
1836 for ($try = 0; $try < $maxRetry; $try++) {
1837 try {
1838 $this->db[$server]->exec($sql);
1839 return 1;
1840 } catch (PDOException $e) {
1841 if (empty($this->_tx[$server]) && $this->isRetryable($e) && $try < $maxRetry - 1) {
1842 usleep(120000 + random_int(0, 150000));
1843 continue;
1844 }
1845
1846 $this->report_db_error('sql', $server, 'SQL-Fehler', $e->getMessage(), 'sql-exec|' . $server);
1847 return 0;
1848 }
1849 }
1850
1851 return 0;
1852 }
1853
1887 public function select_query(string $server, string $sql, string $dd = ''): array|int {
1888 $timers = $this->db_timers_start('select', $dd);
1889
1890 try {
1891 $stmt = $this->query($server, $sql);
1892
1893 if (!is_object($stmt)) {
1894 if ($this->get_error_status() === '') {
1895 $this->report_db_error('sql', $server, 'SQL-Fehler', 'SELECT fehlgeschlagen', 'sql-select|' . $server);
1896 }
1897 return 0;
1898 }
1899
1900 return $stmt->fetchAll(PDO::FETCH_ASSOC);
1901 } catch (PDOException | Exception $e) {
1902 $this->report_db_error('sql', $server, 'SQL-Fehler', $e->getMessage(), 'sql-select|' . $server);
1903 return 0;
1904 } finally {
1905 $this->db_timers_stop($timers);
1906 }
1907 }
1908
1916 private function array_stripslashes(array|string $data): array|string {
1917 if (is_array($data)) {
1918 foreach ($data as $key => $value) {
1919 $data[$key] = $this->array_stripslashes($value);
1920 }
1921 }
1922
1923 if (is_string($data)) {
1924 $data = stripslashes($data);
1925 }
1926
1927 return $data;
1928 }
1929
1949 public function create_select(string $select, array $flds, int $and = 1): string {
1950 $searchTerms = array_filter(explode(' ', trim($select)));
1951
1952 if (!$searchTerms) {
1953 return '1=1';
1954 }
1955
1956 $server = $this->_server ?: 'default';
1957
1958 $whereConditions = [];
1959
1960 foreach ($searchTerms as $term) {
1961 $escaped = $this->escape($term, $server);
1962
1963 $orParts = [];
1964
1965 foreach (array_keys($flds) as $field) {
1966 $orParts[] = "$field LIKE '%$escaped%'";
1967 }
1968
1969 $whereConditions[] = '(' . implode(' OR ', $orParts) . ')';
1970 }
1971
1972 return implode($and ? ' AND ' : ' OR ', $whereConditions);
1973 }
1974
1983 public function insert_query($server, $sql) {
1984 $this->_insert_id = 0;
1985 $retval = -2;
1986
1987 $timers = $this->db_timers_start('save');
1988
1989 try {
1990 $stmt = $this->query($server, $sql);
1991 } finally {
1992 $this->db_timers_stop($timers);
1993 }
1994
1995 if ($stmt) {
1996 $retval = $this->db[$server]->lastInsertId();
1997 }
1998
1999 if ($retval > 0) {
2000 $this->_insert_count++;
2001 }
2002
2003 $this->_insert_id = $retval;
2004
2005 return $retval;
2006 }
2007
2017 public function delete_query($server, $sql) {
2018 $count = -2;
2019 $this->_delete_count = 0;
2020
2021 $stmt = $this->query($server, $sql);
2022
2023 if ($stmt) {
2024 $count = $stmt->rowCount();
2025 }
2026
2027 dbx()->debug("delete count server=($server) count=($count) sql=($sql)");
2028
2029 $this->_delete_count = $count;
2030
2031 return $count;
2032 }
2033
2042 public function exec_query($server, $sql) {
2043 $ok = $this->exec($server, $sql);
2044 return $ok;
2045 }
2046
2065 public function rawQuery(string $server, string $query): mixed {
2066 if (!$server) {
2067 return 0;
2068 }
2069
2070 $iChars = 6;
2071 $connect = $this->connect_db_server($server);
2072 $pos = strpos($query, ' ') ?: $iChars;
2073 $queryType = strtoupper(substr(trim($query), 0, min($pos, $iChars)));
2074
2075 if (!$connect) {
2076 if ($this->get_error_status() === '') {
2077 $this->report_db_error('db', $server, 'Datenbank nicht vorhanden', 'Server not connected', 'db-raw|' . $server);
2078 }
2079 $this->_query = $query;
2080 return 0;
2081 }
2082
2083 try {
2084 if ($queryType === 'SELECT' || $queryType === 'PRAGMA' || $queryType === 'SHOW') {
2085 return $this->select_query($server, $query);
2086 }
2087
2088 if ($queryType === 'INSERT') {
2089 return $this->insert_query($server, $query);
2090 }
2091
2092 if ($queryType === 'UPDATE') {
2093 return $this->update_query($server, $query);
2094 }
2095
2096 if ($queryType === 'DELETE') {
2097 return $this->delete_query($server, $query);
2098 }
2099
2100 $result = $this->exec_query($server, $query);
2101
2102 return $result;
2103 } catch (PDOException $e) {
2104 $this->_query = $query;
2105 $this->report_db_error('sql', $server, 'SQL-Fehler', $e->getMessage(), 'sql-raw|' . $server);
2106
2107 return 0;
2108 }
2109 }
2110
2141 public function select(string $dd = '', $where = '', $columns = '*', $orderby = '', $asc_desc = 'ASC', $groupby = '', $max = 0, $offset = 0, $verify_access = 1) {
2142 $this->clear_db_error();
2143
2144 $owner = 0;
2145 $access = 1;
2146 $fields = '';
2147
2148 $dbtab = $this->get_dd_table($dd);
2149 $server = $this->get_dd_server($dd);
2150 $dbType = $this->get_db_type($server);
2151
2152 if ($verify_access) {
2153 $access = $this->check_access('select', $dd);
2154
2155 if ($access == 0) {
2156 $this->report_db_error('access', (string)$dd, 'Zugriff verweigert', 'select', 'access-select|' . $dd);
2157 return 0;
2158 }
2159
2160 if ($access == 2) {
2161 $owner = 1;
2162 }
2163 }
2164
2165 $where = $this->normalize_where($dd, $where, $owner);
2166
2167
2168 if (!is_array($columns)) {
2169 $columns = strpos($columns, ',') !== false ? explode(',', $columns) : [$columns];
2170 }
2171
2172 foreach ($columns as $no => $field) {
2173 $xfield = is_int($no) ? trim($field) : trim($no);
2174
2175 if ($xfield === '*') {
2176 $fields = '*';
2177 break;
2178 }
2179
2180 if ($this->is_fld_name($dd, $xfield)) {
2181 $fields .= $xfield . ',';
2182 }
2183 }
2184
2185 $fields = $fields === '*' ? '*' : (rtrim($fields, ',') ?: '*');
2186
2187 $query = "SELECT $fields FROM $dbtab ";
2188
2189 if ($where) {
2190 $query .= "WHERE $where ";
2191 }
2192
2193 if ($groupby) {
2194 $query .= "GROUP BY $groupby ";
2195 }
2196
2197 if ($orderby) {
2198 $orderby = trim($orderby);
2199 $orderby = rtrim($orderby, ';');
2200
2201 if (stripos($orderby, ' ASC') !== false || stripos($orderby, ' DESC') !== false) {
2202 $query .= "ORDER BY $orderby ";
2203 } else {
2204 $query .= "ORDER BY $orderby $asc_desc ";
2205 }
2206 }
2207
2208 if ($max > 0) {
2209 if ($dbType === 'mysql') {
2210 $query .= "LIMIT $offset, $max ";
2211 } else {
2212 $query .= "LIMIT $max OFFSET $offset ";
2213 }
2214 }
2215
2216 return $this->select_query($server, $query, $dd);
2217 }
2218
2232 public function select1(string $dd, $where = '', $columns = '*', $verify_access = 1) {
2233 $db_records = $this->select($dd, $where, $columns, '', 'ASC', '', 1, 0, $verify_access);
2234
2235 if (!is_array($db_records)) {
2236 return 0;
2237 }
2238
2239 if (!isset($db_records[0]) || !is_array($db_records[0])) {
2240 return $this->empty_record($dd)[0];
2241 }
2242
2243 return $db_records[0];
2244 }
2245
2259 public function select_tree(string $folder_dd, string $item_dd = '', array $opt = []): array {
2260 $tree_def = $this->get_dd_table_def($folder_dd);
2261 if (!is_array($tree_def)) $tree_def = [];
2262
2263 if ($item_dd === '' && !empty($tree_def['tree_items_dd'])) {
2264 $item_dd = (string)$tree_def['tree_items_dd'];
2265 }
2266
2267 $folder_id = (string)($opt['folder_id'] ?? $tree_def['tree_id'] ?? 'id');
2268 $folder_parent = (string)($opt['folder_parent'] ?? $tree_def['tree_parent'] ?? 'parent_id');
2269 $folder_title = (string)($opt['folder_title'] ?? $tree_def['tree_label'] ?? 'name');
2270 $folder_rights = (string)($opt['folder_rights'] ?? $tree_def['tree_rights'] ?? 'group_read');
2271 $folder_order = (string)($opt['folder_order'] ?? $tree_def['tree_order'] ?? $folder_title);
2272 $folder_where = (string)($opt['folder_where'] ?? '');
2273
2274 $item_id = (string)($opt['item_id'] ?? $tree_def['tree_items_id'] ?? 'id');
2275 $item_parent = (string)($opt['item_parent'] ?? $tree_def['tree_items_parent'] ?? 'folder');
2276 $item_title = (string)($opt['item_title'] ?? $tree_def['tree_items_label'] ?? 'title');
2277 $item_rights = (string)($opt['item_rights'] ?? $tree_def['tree_items_rights'] ?? 'group_read');
2278 $item_order = (string)($opt['item_order'] ?? $tree_def['tree_items_order'] ?? $item_title);
2279 $item_where = (string)($opt['item_where'] ?? '');
2280
2281 $root = (int)($opt['root'] ?? 0);
2282 $verify_access = (int)($opt['verify_access'] ?? 1);
2283
2284 $folder_cols = $this->tree_columns($folder_dd, [
2285 $folder_id,
2286 $folder_parent,
2287 $folder_title,
2288 $folder_rights,
2289 'template',
2290 'module',
2291 'sorter',
2292 'active',
2293 'activ'
2294 ]);
2295
2296 $folders = $this->select($folder_dd, $folder_where, $folder_cols, $folder_order, 'ASC', '', 0, 0, $verify_access);
2297 if (!is_array($folders)) {
2298 $folders = [];
2299 }
2300
2301 $item_cols = [];
2302 $items = [];
2303
2304 if ($item_dd !== '') {
2305 $item_cols = $this->tree_columns($item_dd, [
2306 $item_id,
2307 $item_parent,
2308 $item_title,
2309 $item_rights,
2310 'permalink',
2311 'template',
2312 'description',
2313 'sorter',
2314 'active',
2315 'activ',
2316 'hits',
2317 'update_date'
2318 ]);
2319
2320 $items = $this->select($item_dd, $item_where, $item_cols, $item_order, 'ASC', '', 0, 0, $verify_access);
2321 if (!is_array($items)) {
2322 $items = [];
2323 }
2324 }
2325
2326 $nodes = [];
2327 $byParent = [];
2328 $flat = [];
2329
2330 foreach ($folders as $row) {
2331 if (!is_array($row)) continue;
2332
2333 $id = (int)($row[$folder_id] ?? 0);
2334 if ($id <= 0) continue;
2335
2336 $parent = (int)($row[$folder_parent] ?? 0);
2337 if ($parent === $id) {
2338 $parent = 0;
2339 }
2340
2341 $node = $row;
2342 $node['_node_id'] = 'folder-' . $id;
2343 $node['_type'] = 'folder';
2344 $node['_id'] = $id;
2345 $node['_parent'] = $parent;
2346 $node['_title'] = (string)($row[$folder_title] ?? ('Ordner ' . $id));
2347 $node['_rights'] = (string)($row[$folder_rights] ?? '');
2348 $node['_children'] = [];
2349
2350 $byParent[$parent][] = $node;
2351 }
2352
2353 foreach ($items as $row) {
2354 if (!is_array($row)) continue;
2355
2356 $id = (int)($row[$item_id] ?? 0);
2357 if ($id <= 0) continue;
2358
2359 $parent = (int)($row[$item_parent] ?? 0);
2360
2361 $node = $row;
2362 $node['_node_id'] = 'page-' . $id;
2363 $node['_type'] = 'page';
2364 $node['_id'] = $id;
2365 $node['_parent'] = $parent;
2366 $node['_title'] = (string)($row[$item_title] ?? ('Seite ' . $id));
2367 $node['_rights'] = (string)($row[$item_rights] ?? '');
2368 $node['_children'] = [];
2369
2370 $byParent[$parent][] = $node;
2371 }
2372
2373 $build = function ($parent, $level) use (&$build, &$byParent, &$flat) {
2374 $children = $byParent[$parent] ?? [];
2375 $out = [];
2376
2377 foreach ($children as $node) {
2378 $node['_level'] = $level;
2379
2380 if (($node['_type'] ?? '') === 'folder') {
2381 $node['_children'] = $build((int)$node['_id'], $level + 1);
2382 }
2383
2384 $flat[] = $node;
2385 $out[] = $node;
2386 }
2387
2388 return $out;
2389 };
2390
2391 $nodes = $build($root, 0);
2392
2393 return [
2394 'nodes' => $nodes,
2395 'flat' => $flat,
2396 'folders' => array_values($folders),
2397 'items' => array_values($items),
2398 ];
2399 }
2400
2401 private function tree_columns(string $dd, array $columns): array {
2402 $out = [];
2403
2404 foreach ($columns as $field) {
2405 $field = trim((string)$field);
2406 if ($field === '' || isset($out[$field])) continue;
2407 if ($this->is_fld_name($dd, $field)) {
2408 $out[$field] = $field;
2409 }
2410 }
2411
2412 return array_values($out);
2413 }
2414
2426 function get_new_record(string $dd, array $field_values = []): array {
2427 $empty = $this->empty_record($dd)[0] ?? [];
2428
2429 foreach ($empty as $field => $value) {
2430 if (!isset($field_values[$field])) {
2431 $field_values[$field] = $value;
2432 }
2433 }
2434
2435 if (($field_values['id'] ?? 0) <= 0) {
2436 unset($field_values['id']);
2437 }
2438
2439 return $field_values;
2440 }
2441
2474 function insert($dd, $field_values, $verify_access = 1, $verify_fields = 1, $verify_values = 1, $trace = 1) {
2475 $this->clear_db_error();
2476 $this->_insert_id = 0;
2477 $this->_validation_error = 0;
2478 $this->_validation_warning = 0;
2479
2480 $server = $this->get_dd_server($dd);
2481 $tab = $this->get_dd_table($dd);
2482
2483 if ($dd !== 'dbxTrace') {
2484 $uid = dbx()->user();
2485 $now = $this->now_ms();
2486
2487 $field_values['update_date'] = $now;
2488 $field_values['update_uid'] = $uid;
2489
2490 $field_values += [
2491 'create_date' => $now,
2492 'create_uid' => $uid,
2493 'owner' => $uid
2494 ];
2495 }
2496
2497 if ($dd !== 'dbxTrace') {
2498 $field_values = $this->get_new_record($dd, $field_values);
2499 }
2500
2501 if ($verify_access && ($this->check_access('insert', $dd) !== 1)) {
2502 $this->report_db_error('access', (string)$dd, 'Zugriff verweigert', 'insert', 'access-insert|' . $dd);
2503 return 0;
2504 }
2505
2506 if ($verify_fields) {
2507 $field_values = $this->check_fields($dd, $field_values);
2508 }
2509
2510 if ($verify_values) {
2511 $field_values = $this->check_values($dd, $field_values);
2512 }
2513
2514 if ($this->_validation_error) {
2515 return 0;
2516 }
2517
2518 if (isset($field_values['id']) && $field_values['id'] == 0) {
2519 unset($field_values['id']);
2520 }
2521
2522 if (!$this->connect_db_server($server)) {
2523 if ($this->get_error_status() === '') {
2524 $this->report_db_error('db', (string)$server, 'Datenbank nicht vorhanden', (string)$server, 'db-insert|' . $server);
2525 }
2526 return 0;
2527 }
2528
2529 $fields = array_keys($field_values);
2530 $placeholders = array_fill(0, count($fields), '?');
2531
2532 $sql = "INSERT INTO $tab (" .
2533 implode(',', $fields) .
2534 ") VALUES (" .
2535 implode(',', $placeholders) .
2536 ")";
2537
2538 $maxRetry = 5;
2539
2540 for ($try = 0; $try < $maxRetry; $try++) {
2541 try {
2542 $timers = $this->db_timers_start('save', (string) $dd);
2543
2544 try {
2545 $stmt = $this->db[$server]->prepare($sql);
2546 $stmt->execute(array_values($field_values));
2547
2548 $id = $this->db[$server]->lastInsertId();
2549 } finally {
2550 $this->db_timers_stop($timers);
2551 }
2552
2553 if (!$id) {
2554 $pk = $this->get_dd_primary($dd);
2555
2556 if (!$id && !empty($field_values[$pk])) {
2557 $id = $field_values[$pk];
2558 }
2559 }
2560
2561 $this->_insert_id = $id;
2562
2563 if ($id > 0 && $dd !== 'dbxTrace') {
2564 $this->_insert_count++;
2565 }
2566
2567 if ($trace && $dd !== 'dbxTrace') {
2568 $table_def = $this->get_dd_table_def($dd);
2569 $write_trace = $table_def['trace'] ?? 0;
2570
2571 if ($write_trace) {
2572 $uid = dbx()->user();
2573 $now = $this->now_ms();
2574 $modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
2575 $run1 = dbx()->get_modul_var('dbx_run1');
2576 $run2 = dbx()->get_modul_var('dbx_run2');
2577 $run3 = dbx()->get_modul_var('dbx_run3');
2578 $source = ($uid > 0) ? 'user' : 'system';
2579
2580 $traceData = [
2581 'create_date' => $now,
2582 'create_uid' => $uid,
2583 'update_date' => $now,
2584 'update_uid' => $uid,
2585 'owner' => $uid,
2586 'action' => 'insert',
2587 'dd' => $dd,
2588 'record_id' => $id,
2589 'data_json' => json_encode([
2590 'action' => 'insert',
2591 'dd' => $dd,
2592 'table' => $tab,
2593 'uid' => $uid,
2594 'source' => $source,
2595 'modul' => $modul,
2596 'run1' => $run1,
2597 'run2' => $run2,
2598 'run3' => $run3,
2599 'id' => $id,
2600 'before' => null,
2601 'delta' => $field_values
2602 ], JSON_UNESCAPED_UNICODE)
2603 ];
2604
2605 $trace_result = $this->insert('dbxTrace', $traceData, 0, 0, 0, 0);
2606
2607 if ($trace_result !== 1) {
2608 dbx()->sys_msg(
2609 'warning',
2610 'trace',
2611 'dbxTrace',
2612 'trace insert failed',
2613 json_encode($traceData, JSON_UNESCAPED_UNICODE)
2614 );
2615 }
2616
2617 // Der rekursive Trace-Insert darf die Insert-ID des
2618 // eigentlichen Datensatzes nicht überschreiben.
2619 $this->_insert_id = $id;
2620 }
2621 }
2622
2623 return 1;
2624 } catch (PDOException $e) {
2625 if (empty($this->_tx[$server]) && $this->isRetryable($e) && $try < $maxRetry - 1) {
2626 usleep(120000 + random_int(0, 150000));
2627 continue;
2628 }
2629
2630 $this->report_db_error('sql', (string)$server, 'SQL-Fehler', $e->getMessage(), 'sql-insert|' . $server);
2631
2632 return 0;
2633 }
2634 }
2635
2636 return 0;
2637 }
2638
2669 function update($dd, $field_values, $where, $verify_access = 1, $verify_fields = 1, $verify_values = 1, $trace = 1) {
2670 $this->clear_db_error();
2671 $this->_validation_error = 0;
2672 $this->_validation_warning = 0;
2673
2674 $access = 1;
2675 $owner = 0;
2676 $server = $this->get_dd_server($dd);
2677 $tab = $this->get_dd_table($dd);
2678 $uid = dbx()->user();
2679 $now = $this->now_ms();
2680 $pk = $this->get_dd_primary($dd);
2681
2682 $field_values['update_date'] = $now;
2683 $field_values['update_uid'] = $uid;
2684
2685 if ($verify_access) {
2686 $access = $this->check_access('update', $dd);
2687
2688 if ($access == 2) {
2689 $owner = 1;
2690 }
2691 }
2692
2693 if (!$access) {
2694 $this->report_db_error('access', (string)$dd, 'Zugriff verweigert', 'update', 'access-update|' . $dd);
2695 return 0;
2696 }
2697
2698 $where = $this->normalize_where($dd, $where, $owner);
2699
2700 $beforeRows = [];
2701 $write_trace = 0;
2702 $modul = '';
2703 $run1 = '';
2704 $run2 = '';
2705 $run3 = '';
2706
2707 if ($trace) {
2708 $table_def = $this->get_dd_table_def($dd);
2709 $write_trace = $table_def['trace'] ?? 0;
2710
2711 if ($write_trace) {
2712 $modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
2713 $run1 = dbx()->get_modul_var('dbx_run1');
2714 $run2 = dbx()->get_modul_var('dbx_run2');
2715 $run3 = dbx()->get_modul_var('dbx_run3');
2716 $source = ($uid > 0) ? 'user' : 'system';
2717 $beforeRows = $this->select($dd, $where);
2718 }
2719 }
2720
2721 if ($verify_fields) {
2722 $field_values = $this->check_fields($dd, $field_values);
2723 }
2724
2725 if ($verify_values) {
2726 $field_values = $this->check_values($dd, $field_values);
2727 }
2728
2729 if ($this->_validation_error) {
2730 return 0;
2731 }
2732
2733 if (!$this->connect_db_server($server)) {
2734 if ($this->get_error_status() === '') {
2735 $this->report_db_error('db', (string)$server, 'Datenbank nicht vorhanden', (string)$server, 'db-update|' . $server);
2736 }
2737 return 0;
2738 }
2739
2740 $set = [];
2741 $vals = [];
2742
2743 foreach ($field_values as $field => $value) {
2744 if (is_array($value)) {
2745 $value = $this->get_convert_array($field, $value, 'auto');
2746 }
2747
2748 $set[] = "$field = ?";
2749 $vals[] = $value;
2750 }
2751
2752 $sql = "UPDATE $tab SET " . implode(',', $set) . " WHERE $where";
2753
2754 $maxRetry = 5;
2755
2756 for ($try = 0; $try < $maxRetry; $try++) {
2757 try {
2758 $timers = $this->db_timers_start('save', (string) $dd);
2759
2760 try {
2761 $stmt = $this->db[$server]->prepare($sql);
2762 $stmt->execute($vals);
2763
2764 $count = $stmt->rowCount();
2765 } finally {
2766 $this->db_timers_stop($timers);
2767 }
2768
2769 $this->_update_count = $count;
2770
2771 if ($trace && $write_trace && is_array($beforeRows)) {
2772 foreach ($beforeRows as $row) {
2773 $id = $row[$pk] ?? null;
2774
2775 $delta = [];
2776 $before_changed = [];
2777
2778 foreach ($field_values as $k => $v) {
2779 $old = $row[$k] ?? null;
2780
2781 if ((string) $old !== (string) $v) {
2782 $delta[$k] = $v;
2783 $before_changed[$k] = $old;
2784 }
2785 }
2786
2787 if (!empty($delta)) {
2788 $rec = json_encode([
2789 'action' => 'update',
2790 'dd' => $dd,
2791 'table' => $tab,
2792 'uid' => $uid,
2793 'source' => $source,
2794 'modul' => $modul,
2795 'run1' => $run1,
2796 'run2' => $run2,
2797 'run3' => $run3,
2798 'id' => $id,
2799 'before' => $before_changed,
2800 'delta' => $delta
2801 ], JSON_UNESCAPED_UNICODE);
2802
2803 $this->insert('dbxTrace', [
2804 'create_date' => $now,
2805 'create_uid' => $uid,
2806 'update_date' => $now,
2807 'update_uid' => $uid,
2808 'owner' => $uid,
2809 'action' => 'update',
2810 'dd' => $dd,
2811 'record_id' => $id,
2812 'data_json' => $rec
2813 ], 0, 0, 0, 0);
2814 }
2815 }
2816 }
2817
2818 return 1;
2819 } catch (PDOException $e) {
2820 if (empty($this->_tx[$server]) && $this->isRetryable($e) && $try < $maxRetry - 1) {
2821 usleep(120000 + random_int(0, 150000));
2822 continue;
2823 }
2824
2825 $this->report_db_error('sql', (string)$server, 'SQL-Fehler', $e->getMessage(), 'sql-update|' . $server);
2826
2827 return 0;
2828 }
2829 }
2830
2831 return 0;
2832 }
2833
2843 public function update_query($server, $sql) {
2844 $this->_update_count = 0;
2845 $retval = -2;
2846
2847 $timers = $this->db_timers_start('save');
2848
2849 try {
2850 $stmt = $this->query($server, $sql);
2851 } finally {
2852 $this->db_timers_stop($timers);
2853 }
2854
2855 if ($stmt) {
2856 $retval = $stmt->rowCount();
2857 }
2858
2859 $this->_update_count = $retval;
2860
2861 return $retval;
2862 }
2863
2896 function save($dd, $field_values, $where, $verify_access = 1, $verify_fields = 1, $verify_values = 1, $trace = 1) {
2897 $this->clear_db_error();
2898 $ok = 0;
2899 $owner = 0;
2900 $access = 1;
2901
2902 if ($verify_access) {
2903 $access = $this->check_access('update', $dd);
2904
2905 if ($access == 2) {
2906 $owner = 1;
2907 }
2908 }
2909
2910 $where = $this->check_where($where, $owner, $dd);
2911
2912 if ($where) {
2913 $ok = $this->update($dd, $field_values, $where, $verify_access, $verify_fields, $verify_values, $trace);
2914
2915 if ($ok === 1) {
2916 if ($this->_update_count > 0) {
2917 return 1;
2918 }
2919
2920 $exists = $this->count($dd, $where);
2921 if ($this->get_error_status() !== '') {
2922 return 0;
2923 }
2924 if ($exists > 0) {
2925 return 1;
2926 }
2927 } else {
2928 return 0;
2929 }
2930 }
2931
2932 $ok = $this->insert($dd, $field_values, $verify_access, $verify_fields, $verify_values, $trace);
2933
2934 if (!$ok) {
2935 dbx()->debug("#SAVE# ($ok) dd=($dd) W=($where)");
2936 dbx()->debug("#SAVE# Fields", $field_values);
2937 }
2938
2939 return $ok;
2940 }
2941
2960 function delete($dd, $where, $verify_access = 1, $trace = 1) {
2961 $ok = -1;
2962 $access = 1;
2963 $owner = 0;
2964 $server = $this->get_dd_server($dd);
2965 $tab = $this->get_dd_table($dd);
2966
2967 if (!$where) {
2968 dbx()->sys_msg(
2969 'warning',
2970 'db',
2971 $dd,
2972 'empty where',
2973 'delete blocked'
2974 );
2975 return 0;
2976 }
2977
2978 if ($verify_access) {
2979 $access = $this->check_access('delete', $dd);
2980
2981 if ($access == 2) {
2982 $owner = 1;
2983 }
2984 }
2985
2986 $beforeRows = [];
2987 $write_trace = 0;
2988 $modul = '';
2989 $run1 = '';
2990 $run2 = '';
2991 $run3 = '';
2992 $uid = 0;
2993 $now = '';
2994
2995 if ($access) {
2996 $where = $this->normalize_where($dd, $where, $owner);
2997
2998 if ($trace) {
2999 $table_def = $this->get_dd_table_def($dd);
3000 $write_trace = $table_def['trace'] ?? 0;
3001
3002 if (!$write_trace) {
3003 $write_trace = $table_def['trash'] ?? 0;
3004 }
3005
3006 if ($write_trace) {
3007 $beforeRows = $this->select($dd, $where);
3008
3009 $uid = dbx()->user();
3010 $now = $this->now_ms();
3011 $modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
3012 $run1 = dbx()->get_modul_var('dbx_run1');
3013 $run2 = dbx()->get_modul_var('dbx_run2');
3014 $run3 = dbx()->get_modul_var('dbx_run3');
3015 $source = ($uid > 0) ? 'user' : 'system';
3016 }
3017 }
3018
3019 $query = 'DELETE FROM ' . $tab . ' WHERE ' . $where . ';';
3020
3021 $count = $this->delete_query($server, $query);
3022
3023 if ($count < 0) {
3024 return -2;
3025 }
3026
3027 $ok = ($count > 0) ? 1 : 0;
3028
3029 if ($trace && $write_trace && is_array($beforeRows)) {
3030 $pk = $this->get_dd_primary($dd);
3031
3032 foreach ($beforeRows as $row) {
3033 $id = $row[$pk] ?? null;
3034
3035 $rec = json_encode([
3036 'action' => 'delete',
3037 'dd' => $dd,
3038 'table' => $tab,
3039 'uid' => $uid,
3040 'source' => $source,
3041 'modul' => $modul,
3042 'run1' => $run1,
3043 'run2' => $run2,
3044 'run3' => $run3,
3045 'id' => $id,
3046 'before' => $row,
3047 'delta' => null
3048 ], JSON_UNESCAPED_UNICODE);
3049
3050 $this->insert('dbxTrace', [
3051 'create_date' => $now,
3052 'create_uid' => $uid,
3053 'update_date' => $now,
3054 'update_uid' => $uid,
3055 'owner' => $uid,
3056 'action' => 'delete',
3057 'dd' => $dd,
3058 'record_id' => $id,
3059 'data_json' => $rec
3060 ], 0, 0, 0, 0);
3061 }
3062 }
3063 }
3064
3065 return $ok;
3066 }
3067
3084 public function count($dd, $where = '', $server = '') {
3085 $count = -1;
3086 $explicitServer = $server !== '';
3087
3088 if ($where == 'new') {
3089 return 0;
3090 }
3091
3092 if (!$server) {
3093 $dbtab = $this->get_dd_table($dd);
3094 }
3095
3096 if ($server) {
3097 $dbtab = $dd;
3098 }
3099
3100 if (!$server) {
3101 $server = $this->get_dd_server($dd);
3102 }
3103
3104 if (!$dbtab || !$server) {
3105 return $count;
3106 }
3107
3108 if (!$explicitServer) {
3109 $access = $this->check_access('select', (string)$dd);
3110 if ($access == 0) {
3111 $this->report_db_error('access', (string)$dd, 'Zugriff verweigert', 'count', 'access-count|' . $dd);
3112 return 0;
3113 }
3114
3115 $where = $this->normalize_where($dd, $where, $access == 2 ? 1 : 0);
3116 } else {
3117 $where = is_array($where) ? '' : $where;
3118 }
3119
3120 $query = "SELECT COUNT(*) AS cnt FROM $dbtab";
3121
3122 if (!empty($where)) {
3123 $query .= " WHERE $where";
3124 }
3125
3126 $timers = $this->db_timers_start('select', $explicitServer ? '' : (string) $dd);
3127
3128 try {
3129 $stmt = $this->query($server, $query);
3130
3131 if (!is_object($stmt)) {
3132 if ($this->get_error_status() === '') {
3133 $this->report_db_error('sql', (string)$server, 'SQL-Fehler', 'COUNT fehlgeschlagen', 'sql-count|' . $server);
3134 }
3135 return -1;
3136 }
3137
3138 $row = $stmt->fetch(PDO::FETCH_ASSOC);
3139
3140 if (is_array($row) && isset($row['cnt'])) {
3141 $count = (int) $row['cnt'];
3142 } else {
3143 $count = 0;
3144 }
3145 } catch (PDOException | Exception $e) {
3146 $this->report_db_error('sql', (string)$server, 'SQL-Fehler', $e->getMessage(), 'sql-count|' . $server);
3147 $count = -1;
3148 } finally {
3149 $this->db_timers_stop($timers);
3150 }
3151
3152 return $count;
3153 }
3154
3162 protected function sqlite_sequence_exists(string $server): bool {
3163 if (strtolower((string)$this->get_db_type($server)) !== 'sqlite') {
3164 return false;
3165 }
3166
3167 $rows = $this->select_query(
3168 $server,
3169 "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence' LIMIT 1"
3170 );
3171
3172 return is_array($rows) && count($rows) > 0;
3173 }
3174
3184 public function empty($dd) {
3185 $ok = $this->load_dd($dd);
3186 $server = $this->get_dd_server($dd);
3187 $dbtab = $this->get_dd_table($dd);
3188 $dbType = $this->get_db_type($server);
3189
3190 dbx()->debug("#empty dd=($dd) Server=($server) Tab=($dbtab) Type=($dbType)");
3191
3192 if (!$dbtab || !$server || !$dbType) {
3193 return 0;
3194 }
3195
3196 switch ($dbType) {
3197 case 'mysql':
3198 $sql = "TRUNCATE TABLE $dbtab";
3199 $ok = $this->rawQuery($server, $sql);
3200
3201 if ($ok) {
3202 $sql = "ALTER TABLE $dbtab AUTO_INCREMENT = 1";
3203 $this->rawQuery($server, $sql);
3204 }
3205 break;
3206
3207 case 'sqlite':
3208 dbx()->debug("#truncate Server=($server) Tab=($dbtab)");
3209
3210 $sql = "DELETE FROM $dbtab";
3211 $ok = $this->exec_query($server, $sql);
3212
3213 if ($ok && $this->sqlite_sequence_exists($server)) {
3214 $sql = "DELETE FROM sqlite_sequence WHERE name='$dbtab'";
3215 $this->exec_query($server, $sql);
3216 }
3217
3218 dbx()->debug("truncate=($ok)");
3219 break;
3220
3221 case 'pgsql':
3222 $sql = "TRUNCATE TABLE $dbtab RESTART IDENTITY";
3223 $ok = $this->rawQuery($server, $sql);
3224 break;
3225
3226 case 'sqlsrv':
3227 $sql = "TRUNCATE TABLE $dbtab";
3228 $ok = $this->rawQuery($server, $sql);
3229 break;
3230
3231 case 'oci':
3232 case 'firebird':
3233 case 'cubrid':
3234 case 'dblib':
3235 case 'ibm':
3236 case 'informix':
3237 case 'odbc':
3238 $sql = "DELETE FROM $dbtab";
3239 $ok = $this->rawQuery($server, $sql);
3240 break;
3241
3242 default:
3243 dbx()->sys_msg(
3244 'warning',
3245 'dd',
3246 $dd,
3247 "no type def ($dbType)",
3248 'check'
3249 );
3250
3251 $ok = 0;
3252 }
3253
3254 return $ok;
3255 }
3256
3268 public function delete_tab($dd) {
3269 $ok = $this->empty($dd);
3270
3271 if (!$ok) {
3272 return 0;
3273 }
3274
3275 return $this->optimize_tab($dd);
3276 }
3277
3288 public function optimize_tab($dd) {
3289 $this->load_dd($dd);
3290 $server = $this->get_dd_server($dd);
3291 $dbtab = $this->get_dd_table($dd);
3292 $dbType = strtolower((string)$this->get_db_type($server));
3293
3294 if (!$server || !$dbtab || !$dbType) {
3295 return 0;
3296 }
3297
3298 switch ($dbType) {
3299 case 'mysql':
3300 $table = $this->quote_db_identifier_for_type($dbType, $dbtab);
3301 return (int)$this->exec($server, 'OPTIMIZE TABLE ' . $table);
3302
3303 case 'sqlite':
3304 return (int)$this->exec($server, 'VACUUM');
3305
3306 default:
3307 dbx()->debug("#optimize tab skipped dd=($dd) Server=($server) Tab=($dbtab) Type=($dbType)");
3308 return 1;
3309 }
3310 }
3311
3318 private function resolve_sqlite_db_path(string $server): string {
3319 if (!preg_match('/\.(db3|sqlite|sqlite3)$/i', $server)) {
3320 return '';
3321 }
3322
3323 $activ_modul = dbx()->get_system_var('dbx_activ_modul', 'dbx');
3324 $sqlite_modul = '';
3325 $sqlite_name = '';
3326
3327 if (strpos($server, '|') !== false) {
3328 $parts = explode('|', $server, 2);
3329 $sqlite_modul = trim($parts[0]);
3330 $sqlite_name = trim($parts[1]);
3331
3332 if ($sqlite_modul === 'modul' || $sqlite_modul === '') {
3333 $sqlite_modul = $activ_modul;
3334 }
3335 } else {
3336 $sqlite_modul = $activ_modul;
3337 $sqlite_name = $server;
3338 }
3339
3340 $file1 = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/dbx/db/' . $sqlite_name);
3341 $file2 = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/' . $sqlite_modul . '/db/' . $sqlite_name);
3342
3343 if (is_file($file1)) {
3344 return $file1;
3345 }
3346 if (is_file($file2)) {
3347 return $file2;
3348 }
3349
3350 return $file2;
3351 }
3352
3362 private function log_db_schema_issue(string $key, string $rid, string $why, string $what): void {
3363 if ($key === '') {
3364 return;
3365 }
3366
3367 $this->report_db_error('db', $rid, $why, $what, $key);
3368 }
3369
3383 function get_table_exist($dd, $dbtab = '') {
3384 $rid = (string)$dd;
3385
3386 if (!$dbtab) {
3387 $dbtab = $this->get_dd_table($dd);
3388 $server = $this->get_dd_server($dd);
3389 $rid = (string)$dd;
3390 } else {
3391 $server = $dd;
3392 $rid = (string)$server;
3393 }
3394
3395 if (!$server || !$dbtab) {
3396 return 0;
3397 }
3398
3399 if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', (string)$dbtab)) {
3400 return 0;
3401 }
3402
3403 $dbType = $this->get_db_type($server);
3404
3405 if ($dbType === 'sqlite') {
3406 $dbFile = $this->resolve_sqlite_db_path($server);
3407 if ($dbFile !== '' && !is_file($dbFile)) {
3408 $this->log_db_schema_issue(
3409 'db-missing|' . $server,
3410 $rid,
3411 'Datenbank nicht vorhanden',
3412 $dbFile
3413 );
3414 return 0;
3415 }
3416 }
3417
3418 if (!$this->connect_db_server($server)) {
3419 $this->log_db_schema_issue(
3420 'db-connect|' . $server,
3421 $rid,
3422 'Datenbank nicht vorhanden',
3423 trim((string)$this->_dbMessage) !== '' ? (string)$this->_dbMessage : (string)$server
3424 );
3425 return 0;
3426 }
3427
3428 if (!isset($this->db[$server]) || !is_object($this->db[$server])) {
3429 $this->log_db_schema_issue(
3430 'db-handle|' . $server,
3431 $rid,
3432 'Datenbank nicht vorhanden',
3433 (string)$server
3434 );
3435 return 0;
3436 }
3437
3438 try {
3439 $exists = 0;
3440
3441 switch ($dbType) {
3442 case 'sqlite':
3443 $stmt = $this->db[$server]->prepare(
3444 "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1"
3445 );
3446 $stmt->execute(array($dbtab));
3447 $exists = $stmt->fetchColumn() ? 1 : 0;
3448 break;
3449
3450 case 'mysql':
3451 $stmt = $this->db[$server]->prepare('SHOW TABLES LIKE ?');
3452 $stmt->execute(array($dbtab));
3453 $exists = $stmt->fetchColumn() ? 1 : 0;
3454 break;
3455
3456 case 'pgsql':
3457 $stmt = $this->db[$server]->prepare(
3458 "SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = ? LIMIT 1"
3459 );
3460 $stmt->execute(array($dbtab));
3461 $exists = $stmt->fetchColumn() ? 1 : 0;
3462 break;
3463
3464 case 'oci':
3465 $stmt = $this->db[$server]->prepare(
3466 'SELECT 1 FROM user_tables WHERE table_name = ? AND ROWNUM = 1'
3467 );
3468 $stmt->execute(array(strtoupper($dbtab)));
3469 $exists = $stmt->fetchColumn() ? 1 : 0;
3470 break;
3471
3472 case 'sqlsrv':
3473 $stmt = $this->db[$server]->prepare(
3474 "SELECT 1 FROM information_schema.tables WHERE table_name = ?"
3475 );
3476 $stmt->execute(array($dbtab));
3477 $exists = $stmt->fetchColumn() ? 1 : 0;
3478 break;
3479
3480 default:
3481 $stmt = $this->db[$server]->prepare('SELECT 1 FROM ' . $dbtab . ' WHERE 1 = 0 LIMIT 1');
3482 $stmt->execute();
3483 $exists = 1;
3484 break;
3485 }
3486
3487 if (!$exists) {
3488 $why = 'Tabelle nicht vorhanden';
3489 $what = $dbtab . ' (' . $server . ')';
3490
3491 if ($dbType === 'sqlite') {
3492 $countStmt = $this->db[$server]->query(
3493 "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
3494 );
3495 $tableCount = $countStmt ? (int)$countStmt->fetchColumn() : 0;
3496 if ($tableCount === 0) {
3497 $why = 'Datenbank nicht vorhanden';
3498 $what = $this->resolve_sqlite_db_path($server) ?: (string)$server;
3499 }
3500 }
3501
3502 $this->log_db_schema_issue(
3503 'table-missing|' . $server . '|' . $dbtab,
3504 $rid,
3505 $why,
3506 $what
3507 );
3508 }
3509
3510 return $exists;
3511 } catch (PDOException $e) {
3512 $this->log_db_schema_issue(
3513 'db-schema|' . $server . '|' . $dbtab,
3514 $rid,
3515 'Datenbank nicht vorhanden',
3516 trim((string)$e->getMessage()) !== '' ? (string)$e->getMessage() : ($dbtab . ' (' . $server . ')')
3517 );
3518 return 0;
3519 }
3520 }
3521
3529 function get_dd_exist($dd) {
3530 $dd_file = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/dbx/dd/' . $dd . '.dd.php');
3531 $exist = file_exists($dd_file);
3532 return $exist;
3533 }
3534
3552 public function get_db_tables($server, $not = 'sqlite_sequence') {
3553 $tables = array();
3554 $ok = $this->connect_db_server($server);
3555
3556 if ($ok) {
3557 $dbType = $this->get_db_type($server);
3558 $tableRows = array();
3559
3560 switch ($dbType) {
3561 case 'mysql':
3562 $sql = "SHOW TABLES";
3563 $tableRows = $this->rawQuery($server, $sql);
3564 break;
3565
3566 case 'sqlite':
3567 $sql = "SELECT name FROM sqlite_master"
3568 . " WHERE type='table' AND name NOT LIKE 'sqlite_%'";
3569 $tableRows = $this->rawQuery($server, $sql);
3570 break;
3571
3572 case 'oci':
3573 $sql = "SELECT table_name FROM user_tables";
3574 $tableRows = $this->rawQuery($server, $sql);
3575 break;
3576
3577 case 'pgsql':
3578 $sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'";
3579 $tableRows = $this->rawQuery($server, $sql);
3580 break;
3581
3582 case 'sqlsrv':
3583 $sql = "SELECT table_name FROM information_schema.tables";
3584 $tableRows = $this->rawQuery($server, $sql);
3585 break;
3586
3587 default:
3588 $tableRows = array();
3589 dbx()->sys_msg('warning', 'db', $server, "unsupported db type ($dbType)", 'get_db_tables');
3590 break;
3591 }
3592
3593 if (is_array($tableRows)) {
3594 foreach ($tableRows as $tableRow) {
3595 $tableName = reset($tableRow);
3596
3597 if ($tableName != $not) {
3598 $count = $this->count($tableName, '', $server);
3599
3600 if ($count < 0) {
3601 $count = -1;
3602 }
3603
3604 $tables[] = array(
3605 'server' => $server,
3606 'name' => $tableName,
3607 'count' => $count
3608 );
3609 }
3610 }
3611 }
3612 }
3613
3614 return $tables;
3615 }
3616
3626 public function get_dd_table_def($dd) {
3627 $table = 0;
3628 dbx()->debug("F-Load dd=($dd)");
3629
3630 $dd_sys = $this->load_dd($dd);
3631 $dd_status = $dd_sys['dd_status'] ?? 0;
3632 $dd_modul = $dd_sys['dd_modul'] ?? '';
3633 $dd_name = $dd_sys['dd_name'] ?? '';
3634
3635 if ($dd_status == 1) {
3636 $table = $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['table'] ?? 0;
3637 }
3638
3639 return $table;
3640 }
3641
3659 public function get_dd_fields($dd, $label = 0) {
3660 $fields = 0;
3661
3662 $dd_sys = $this->load_dd($dd);
3663 $dd_status = $dd_sys['dd_status'] ?? 0;
3664 $dd_modul = $dd_sys['dd_modul'] ?? '';
3665 $dd_name = $dd_sys['dd_name'] ?? '';
3666
3667 if ($dd_status == 1) {
3668 $fields = $_SESSION['dbx']['cache']['dd'][$dd_modul][$dd_name]['fields'];
3669 }
3670
3671 if ($label && $dd_status == 1) {
3672 $xfields = array();
3673
3674 if (is_array($fields)) {
3675 foreach ($fields as $no => $field) {
3676 $xname = $field['name'] ?? '';
3677 $xlabel = $xname;
3678
3679 if ($xname === '') {
3680 continue;
3681 }
3682
3683 if ($label == 1 && isset($field['label']) && $field['label'] !== '') {
3684 $xlabel = $field['label'];
3685 }
3686
3687 $xfields[$xname] = $xlabel;
3688 }
3689 }
3690
3691 $fields = $xfields;
3692 }
3693
3694 return $fields;
3695 }
3696
3706 public function get_rpt_fields($dd, $flds, $label = 1) {
3707 $fields = $this->get_dd_fields($dd, $label);
3708
3709 if ($flds != '*') {
3710 $cols = array();
3711 $flds = explode(",", $flds);
3712
3713 if (is_array($flds)) {
3714 foreach ($flds as $no => $field) {
3715 $field = trim($field);
3716 $cols[$field] = $field;
3717 }
3718
3719 $flds = $cols;
3720 }
3721 } else {
3722 $flds = $fields;
3723 }
3724
3725 return $flds;
3726 }
3727
3735 public function get_dd_cols(string $dd): string {
3736 $fields = $this->get_dd_fields($dd);
3737
3738 if (!$fields || !is_array($fields)) {
3739 return '';
3740 }
3741
3742 $cols = [];
3743
3744 foreach ($fields as $f) {
3745 if (empty($f['name'])) {
3746 continue;
3747 }
3748
3749 $cols[] = $f['name'];
3750 }
3751
3752 return implode(',', $cols);
3753 }
3754
3770 public function get_dd_grid_cols(string $dd): string {
3771 $fields = $this->get_dd_fields($dd);
3772
3773 if (!$fields || !is_array($fields)) {
3774 return '';
3775 }
3776
3777 $cols = [];
3778
3779 foreach ($fields as $f) {
3780 if (empty($f['name'])) {
3781 continue;
3782 }
3783
3784 $name = $f['name'];
3785
3786 if (str_starts_with($name, '_')) {
3787 continue;
3788 }
3789
3790 $ddType = $f['type'] ?? 'text';
3791 $gridType = $this->map_dd_type_to_grid_type($ddType);
3792
3793 $label = '';
3794 if (!empty($f['label']) && is_string($f['label'])) {
3795 $label = trim($f['label']);
3796 }
3797
3798 $label = str_replace([':', '[', ']'], '-', $label);
3799
3800 if (!$label) {
3801 $label = $name;
3802 }
3803
3804 $fieldWithLabel = $name . '[' . $label . ']';
3805
3806 $protect = isset($f['protect']) ? (string) $f['protect'] : '0';
3807
3808 $group = '';
3809 if (!empty($f['group']) && is_string($f['group'])) {
3810 $group = '@' . trim($f['group']);
3811 }
3812
3813 if ($protect === '2') {
3814 $cols[] = $fieldWithLabel . ':' . $gridType . ':!v' . $group;
3815 continue;
3816 }
3817
3818 if ($protect === '1') {
3819 $cols[] = $fieldWithLabel . ':' . $gridType . ':p' . $group;
3820 continue;
3821 }
3822
3823 $cols[] = $fieldWithLabel . ':' . $gridType . $group;
3824 }
3825
3826 return implode(',', $cols);
3827 }
3828
3836 public function map_dd_type_to_grid_type(string $ddType): string {
3837 $t = strtolower(trim($ddType));
3838
3839 switch ($t) {
3840 case 'int':
3841 case 'integer':
3842 case 'bigint':
3843 case 'smallint':
3844 case 'mediumint':
3845 case 'tinyint':
3846 case 'float':
3847 case 'double':
3848 case 'decimal':
3849 case 'numeric':
3850 case 'real':
3851 return 'number';
3852
3853 case 'date':
3854 case 'datetime':
3855 case 'timestamp':
3856 return 'date';
3857
3858 case 'lookup':
3859 case 'select':
3860 return 'lookup';
3861
3862 case 'varchar':
3863 case 'char':
3864 case 'text':
3865 case 'string':
3866 default:
3867 return 'text';
3868 }
3869 }
3870
3893 function get_convert_array($field, $array, $convert = 'auto') {
3894 $value = '';
3895
3896 if ($convert == 'auto') {
3897 if (is_string($array) && str_starts_with($array, 'a:')) {
3898 $convert = 'serial';
3899 } elseif (is_array($array)) {
3900 foreach ($array as $val) {
3901 if (is_array($val)) {
3902 $convert = 'serial';
3903 break;
3904 }
3905 }
3906
3907 if ($convert == 'auto') {
3908 $convert = 'list';
3909 }
3910 } else {
3911 $convert = 'serial';
3912 }
3913 }
3914
3915 if ($convert == 'serial' && is_string($array)) {
3916 $unserialized = @unserialize($array);
3917
3918 if ($unserialized !== false && is_array($unserialized)) {
3919 $array = $unserialized;
3920
3921 foreach ($array as $val) {
3922 if (is_array($val)) {
3923 return serialize($array);
3924 }
3925 }
3926
3927 $convert = 'list';
3928 }
3929 }
3930
3931 if ($convert == 'list' && is_array($array)) {
3932 $value = implode(',', $array);
3933 } else {
3934 $value = $array;
3935 }
3936
3937 return $value == 'a:0:{}' ? '' : $value;
3938 }
3939
3961 function check_access(string $mode, string $dd) {
3962 dbx()->debug("check-acces Mode=($mode) Tab=($dd)");
3963
3964 if (dbx()->can('dbxRunAsAdmin')) {
3965 return 1;
3966 }
3967
3968 $access = 0;
3969 $groups = '';
3970 $table = $this->get_dd_table($dd, 1);
3971
3972 if (is_array($table)) {
3973 $modeKey = '';
3974 $ownerKey = '';
3975
3976 if ($mode == 'insert') {
3977 $modeKey = 'create';
3978 $ownerKey = 'create_owner';
3979 }
3980
3981 if ($mode == 'update') {
3982 $modeKey = 'update';
3983 $ownerKey = 'update_owner';
3984 }
3985
3986 if ($mode == 'delete') {
3987 $modeKey = 'delete';
3988 $ownerKey = 'delete_owner';
3989 }
3990
3991 if ($mode == 'select') {
3992 $modeKey = 'read';
3993 $ownerKey = 'read_owner';
3994 }
3995
3996 $groups = $modeKey !== '' ? (string)($table[$modeKey] ?? '') : '';
3997 $access = dbx()->can(access_groups: $groups);
3998
3999 if (!$access && dbx()->user() > 0) {
4000 if (preg_match('/\bowner\b/', (string) $groups)) {
4001 $access = $mode === 'insert' ? 1 : 2;
4002 }
4003 }
4004
4005 if (!$access && $ownerKey !== '' && dbx()->user() > 0) {
4006 $ownerGroups = trim((string)($table[$ownerKey] ?? ''));
4007 if ($ownerGroups !== '') {
4008 if ($ownerGroups === '*' || preg_match('/\bowner\b/', $ownerGroups) || dbx()->can(access_groups: $ownerGroups)) {
4009 $access = $mode === 'insert' ? 1 : 2;
4010 $groups = $ownerGroups;
4011 }
4012 }
4013 }
4014 }
4015
4016 dbx()->debug("check-acces dd=($dd) Mode=($mode) Groups=($groups) Access=($access)");
4017
4018 return $access;
4019 }
4020
4042 function check_values($dd, $field_values, $verify_values = 1) {
4043 $db_field_values = [];
4044 $this->_validation_error = 0;
4045 $this->_validation_warning = 0;
4046 $this->_validation_error_flds = [];
4047 $this->_validation_warning_flds = [];
4048
4049 $validate_rules = $this->_validatior_rules;
4050 $validate_type = $this->_validatior_type;
4051 $validate_error = $this->_validatior_error;
4052 $validate_mode = $this->_validatior_mode;
4053
4054 $fields = $this->get_dd_fields($dd);
4055
4056 if (!is_array($fields)) {
4057 return $db_field_values;
4058 }
4059
4060 foreach ($fields as $field) {
4061 $name = $field['name'] ?? '';
4062 $length = $field['length'] ?? 0;
4063 $type = $field['type'] ?? '';
4064 $rules = $field['rules'] ?? '';
4065
4066 if ($name !== '' && isset($field_values[$name])) {
4067 $ok = true;
4068 $value = $field_values[$name];
4069
4070 if ($validate_rules && $rules && $value !== '' && $value !== null) {
4071 $ok = $this->oValidator->validate($value, $rules, $name);
4072 }
4073
4074 if (is_array($value)) {
4075 $value = $this->get_convert_array($name, $value, 'auto');
4076 }
4077
4078 if ($ok && $validate_type && $value !== '' && $value !== null) {
4079 $rules = '';
4080
4081 if ($type && $length > 0) {
4082 $rules = $type . '|max=' . $length;
4083 } elseif ($type) {
4084 $rules = $type;
4085 }
4086
4087 if ($rules) {
4088 $ok = $this->oValidator->validate($value, $rules, $name);
4089 }
4090 }
4091
4092 if (!$ok) {
4093 $err = [
4094 'name' => $name,
4095 'rules' => $rules,
4096 'value' => $value
4097 ];
4098
4099 if ($validate_error) {
4100 $this->_validation_error_flds[] = $err;
4101 $this->_validation_error++;
4102 } else {
4103 $this->_validation_warning_flds[] = $err;
4104 $this->_validation_warning++;
4105 }
4106
4107 if ($validate_mode === 'clean') {
4108 $rules = $field['rules'] ?? '';
4109
4110 if (strpos((string) $rules, 'array') !== false) {
4111 $rules = $type;
4112 } else {
4113 if ($rules === '*') {
4114 $rules = '';
4115 }
4116
4117 if ($rules && $rules !== $type) {
4118 $rules .= '|' . $type;
4119 } else {
4120 $rules = $type;
4121 }
4122 }
4123
4124 $value = $this->oValidator->clean($value, $rules, $length, $name);
4125 }
4126
4127 if ($validate_mode === 'unset') {
4128 $name = false;
4129 }
4130 }
4131
4132 if ($name) {
4133 $db_field_values[$name] = $value;
4134 }
4135 }
4136 }
4137
4138 return $db_field_values;
4139 }
4140
4153 function check_fields($dd, $field_values) {
4154 $db_fields = $this->get_dd_fields($dd);
4155
4156 if (!is_array($db_fields) || empty($db_fields)) {
4157 return array();
4158 }
4159
4160 $valid_names = array();
4161
4162 foreach ($db_fields as $field) {
4163 if (isset($field['name']) && $field['name'] !== '') {
4164 $valid_names[$field['name']] = 1;
4165 }
4166 }
4167
4168 return array_filter($field_values, function ($name) use ($valid_names) {
4169 return isset($valid_names[$name]);
4170 }, ARRAY_FILTER_USE_KEY);
4171 }
4172
4187 function empty_record(string $dd): array {
4188 $dd_fields = $this->get_dd_fields($dd);
4189 $record = [];
4190
4191 if (!is_array($dd_fields)) {
4192 return [$record];
4193 }
4194
4195 foreach ($dd_fields as $field) {
4196 $name = $field['name'] ?? '';
4197
4198 if ($name === '') {
4199 continue;
4200 }
4201
4202 $value = $field['default'] ?? '';
4203
4204 if (!isset($field['default']) && in_array(($field['type'] ?? ''), ['int', 'longint'])) {
4205 $value = 0;
4206 }
4207
4208 $record[$name] = $value;
4209 }
4210
4211 return [$record];
4212 }
4213
4225 private function is_fld_name(string $dd, $xfield) {
4226 $db_fields = $this->get_dd_fields($dd);
4227
4228 if (!is_array($db_fields) || empty($db_fields) || !is_string($xfield) || $xfield === '') {
4229 return false;
4230 }
4231
4232 foreach ($db_fields as $field) {
4233 if (!isset($field['name'])) {
4234 continue;
4235 }
4236
4237 if ($field['name'] === $xfield) {
4238 return true;
4239 }
4240 }
4241
4242 return false;
4243 }
4244
4284 public function normalize_where(string $dd, $where = '', int $owner = 0): string {
4285 if (!is_array($where)) {
4286 return $this->check_where($where, $owner, $dd);
4287 }
4288
4289 $conditions = array();
4290 $server = $this->get_dd_server($dd);
4291
4292 if (isset($where['raw'])) {
4293 if (!empty($where['trusted'])) {
4294 return $this->check_where((string) $where['raw'], $owner, $dd);
4295 }
4296
4297 dbx()->sys_msg('warning', 'db', $dd, 'raw where blocked', (string) $where['raw']);
4298 return $this->check_where('', $owner, $dd);
4299 }
4300
4301 if (isset($where['search']) && is_array($where['search'])) {
4302 $search = $where['search'];
4303 $built = $this->build_search_where(
4304 $dd,
4305 $search['value'] ?? '',
4306 is_array($search['like'] ?? null) ? $search['like'] : array(),
4307 is_array($search['equal'] ?? null) ? $search['equal'] : array(),
4308 (string) ($search['mode'] ?? 'starts_with')
4309 );
4310
4311 if ($built !== '') {
4312 $conditions[] = $built;
4313 }
4314 }
4315
4316 foreach ($where as $field => $value) {
4317 if (in_array($field, array('search', 'raw', 'trusted'), true)) {
4318 continue;
4319 }
4320
4321 $field = trim((string) $field);
4322
4323 if (!$this->is_fld_name($dd, $field)) {
4324 continue;
4325 }
4326
4327 if (is_array($value)) {
4328 $mode = (string) ($value['mode'] ?? 'starts_with');
4329
4330 if (isset($value['like'])) {
4331 $likeValue = $this->escape_like((string) $value['like'], $server);
4332 $pattern = $likeValue . '%';
4333
4334 if ($mode === 'contains') {
4335 $pattern = '%' . $likeValue . '%';
4336 } elseif ($mode === 'ends_with') {
4337 $pattern = '%' . $likeValue;
4338 } elseif ($mode === 'exact') {
4339 $pattern = $likeValue;
4340 }
4341
4342 $conditions[] = "$field LIKE '$pattern' ESCAPE '\\'";
4343 continue;
4344 }
4345
4346 if (array_key_exists('value', $value)) {
4347 $value = $value['value'];
4348 } else {
4349 continue;
4350 }
4351 }
4352
4353 if ($value === null) {
4354 $conditions[] = "$field IS NULL";
4355 continue;
4356 }
4357
4358 if (is_array($value) || is_object($value)) {
4359 continue;
4360 }
4361
4362 if (dbx()->is_int_value($value)) {
4363 $conditions[] = "$field = " . (int) $value;
4364 } else {
4365 $conditions[] = "$field = '" . $this->escape((string) $value, $server) . "'";
4366 }
4367 }
4368
4369 return $this->check_where(implode(' AND ', $conditions), $owner, $dd);
4370 }
4371
4372 function check_where($where, $owner = 0, $dd = '') {
4373 $key = $this->_fld_id ?? 'id';
4374
4375 if ($where == 'new' || $where == '0') {
4376 $where = '';
4377 }
4378
4379 if (dbx()->is_int_value($where)) {
4380 if ($dd) {
4381 $dd_primary = $this->get_dd_primary($dd);
4382
4383 if ($dd_primary) {
4384 $key = $dd_primary;
4385 }
4386 }
4387
4388 $where = "$key = $where";
4389 }
4390
4391 if ($owner) {
4392 $uid = dbx()->user();
4393 $owner_field = 'owner';
4394
4395 if ($dd) {
4396 $table_def = $this->get_dd_table_def($dd);
4397 $configured_owner_field = trim((string)($table_def['owner_field'] ?? ''));
4398 if ($configured_owner_field !== '' && $this->is_fld_name($dd, $configured_owner_field)) {
4399 $owner_field = $configured_owner_field;
4400 }
4401 }
4402
4403 if ($where) {
4404 $where = '(' . $where . ') AND ' . $owner_field . ' = ' . $uid;
4405 } else {
4406 $where = $owner_field . ' = ' . $uid;
4407 }
4408 }
4409
4410 return $where;
4411 }
4412
4426 public function escape($string, $server) {
4427 if (!$this->connect_db_server($server)) {
4428 return str_replace("'", "''", $string);
4429 }
4430
4431 return substr($this->db[$server]->quote($string), 1, -1);
4432 }
4433
4446 public function escape_like($value, $server) {
4447 $value = (string) $value;
4448 $value = str_replace('\\', '\\\\', $value);
4449 $value = str_replace('%', '\%', $value);
4450 $value = str_replace('_', '\_', $value);
4451
4452 return $this->escape($value, $server);
4453 }
4454
4470 public function build_search_where(string $dd, $search, array $likeFields, array $equalFields = array(), string $mode = 'starts_with'): string {
4471 $search = trim((string) $search);
4472
4473 if ($search === '') {
4474 return '';
4475 }
4476
4477 if (!$this->oValidator->validate($search, 'sqlsearch|max=128', 'search')) {
4478 dbx()->sys_msg('warning', 'db', $dd, 'invalid search', $search);
4479 return '';
4480 }
4481
4482 $server = $this->get_dd_server($dd);
4483
4484 if (!$server) {
4485 return '';
4486 }
4487
4488 $likeValue = $this->escape_like($search, $server);
4489 $exactValue = $this->escape($search, $server);
4490 $conditions = array();
4491
4492 foreach ($likeFields as $field) {
4493 $field = trim((string) $field);
4494
4495 if (!$this->is_fld_name($dd, $field)) {
4496 continue;
4497 }
4498
4499 $pattern = $likeValue;
4500
4501 if ($mode === 'contains') {
4502 $pattern = '%' . $likeValue . '%';
4503 } elseif ($mode === 'ends_with') {
4504 $pattern = '%' . $likeValue;
4505 } elseif ($mode !== 'exact') {
4506 $pattern = $likeValue . '%';
4507 }
4508
4509 $conditions[] = "$field LIKE '$pattern' ESCAPE '\\'";
4510 }
4511
4512 foreach ($equalFields as $field) {
4513 $field = trim((string) $field);
4514
4515 if (!$this->is_fld_name($dd, $field)) {
4516 continue;
4517 }
4518
4519 $conditions[] = "$field = '$exactValue'";
4520 }
4521
4522 if (!count($conditions)) {
4523 return '';
4524 }
4525
4526 return '(' . implode(' OR ', $conditions) . ')';
4527 }
4528
4534 private function now_ms(): string {
4535 return (new DateTime())->format('Y-m-d H:i:s.v');
4536 }
4537}
$table['server']
Definition .dd.php:6
$indexes[]
Definition .dd.php:167
Zentrale Datenbank- und DD-Systemklasse von DBX.
insert($dd, $field_values, $verify_access=1, $verify_fields=1, $verify_values=1, $trace=1)
Fügt einen neuen Datensatz in die Datenbank ein.
build_search_where(string $dd, $search, array $likeFields, array $equalFields=array(), string $mode='starts_with')
Baut eine sichere Such-WHERE fuer einfache Report-Suchen.
$_report_error
1 = DB-Fehler automatisch in dbxSysMsg schreiben, 0 = nicht
get_dd_file(string $dd)
Liefert den Dateipfad einer geladenen DD fuer Editor-Marker.
$_validation_warning_flds
get_dd_autosync($dd, $rec=0)
Gibt das Autosync-Flag einer DD zurück.
select_tree(string $folder_dd, string $item_dd='', array $opt=[])
Liefert generische Tree-Daten aus einer Parent-Tabelle und optionalen Items.
save($dd, $field_values, $where, $verify_access=1, $verify_fields=1, $verify_values=1, $trace=1)
Speichert einen Datensatz mit UPSERT-ähnlichem Verhalten.
$_validatior_mode
$_validation_error
get_table_exist($dd, $dbtab='')
Prueft, ob eine Tabelle existiert.
db_server_config_is_active(string $server, array $dbConfig)
Prueft den Aktivstatus eines konfigurierten SQL-Servers.
can_connect_database_config(array $dbConfig, bool $withDatabase=false)
insert_query($server, $sql)
Führt eine INSERT-Abfrage aus und gibt die zuletzt eingefügte ID zurück.
get_error_status()
Liefert die Fehlerart des letzten DB-Aufrufs.
rawQuery(string $server, string $query)
Führt eine rohe SQL-Abfrage auf dem angegebenen Server aus.
sqlite_sequence_exists(string $server)
Prueft ohne Schema-Warnung, ob SQLite seine AUTOINCREMENT-Tabelle hat.
int $_connect_timeout
Maximale Wartezeit fuer einen DB-Verbindungsaufbau in Sekunden.
$_validatior_error
count($dd, $where='', $server='')
Zählt Datensätze einer DD oder Tabelle.
load_dd(string $dd)
Lädt eine Datenbeschreibung (DD) aus Datei und speichert sie im Session-Cache.
__destruct()
Gibt interne Referenzen beim Zerstören des Objekts frei.
get_dd_cols(string $dd)
Gibt alle DD-Spaltennamen als CSV-String zurück.
$_validatior_rules
escape_like($value, $server)
Escaped einen User-Suchwert fuer SQL-LIKE.
get_dd_sort_desc($dd)
Platzhalter für Sortierrichtung einer DD.
optimize_tab($dd)
Optimiert eine Tabelle anhand ihrer DD nach Wartungsaktionen.
empty($dd)
Leert eine Tabelle anhand ihrer DD.
check_values($dd, $field_values, $verify_values=1)
Überprüft und validiert übergebene Feldwerte anhand der DD-Regeln.
exec(string $server, string $sql)
Führt eine SQL-Anweisung aus, die keine Daten zurückgibt (z.
$_validation_error_flds
dbConnect($server, $dbType, $dbHost, $dbName='', $dbUser='', $dbPass='', $dbPort='')
Stellt eine Verbindung zu einer Datenbank her und speichert sie in $this->db[$server].
get_error_text()
Liefert den Fehlertext des letzten DB-Aufrufs.
query(string $server, string $sql)
Führt eine SQL-Abfrage auf dem angegebenen Server aus.
update_query($server, $sql)
Führt eine UPDATE-Abfrage aus und gibt die Anzahl der betroffenen Zeilen zurück.
$_error_status
db|sql|access
select(string $dd='', $where='', $columns=' *', $orderby='', $asc_desc='ASC', $groupby='', $max=0, $offset=0, $verify_access=1)
Führt eine SELECT-Abfrage auf der angegebenen DD aus.
ensure_database_exists(string $server, array $dbConfig)
get_dd_server_binding_info(string $dd)
Liefert Herkunft und Ergebnis der Serveraufloesung einer DD.
begin(string $dd)
Startet eine Transaktion für die Datenbank der angegebenen DD.
delete_tab($dd)
Leert eine Datenbank-Tabelle anhand ihrer DD und setzt die ID zurueck.
get_dd_grid_cols(string $dd)
Erzeugt die neue Grid-Column-Syntax aus einer DD.
get_dd_table_def($dd)
Gibt die komplette Table-Definition einer DD zurück.
normalize_where(string $dd, $where='', int $owner=0)
Normalisiert und erweitert eine WHERE-Bedingung für SQL-Abfragen.
empty_record(string $dd)
Erstellt einen leeren Datensatz mit Standardwerten basierend auf der DD.
is_valid_dd_server_binding(string $server)
Prueft eine lokale DD-Serverbindung, ohne eine Verbindung aufzubauen.
create_select(string $select, array $flds, int $and=1)
Erzeugt eine SQL-WHERE-Bedingung für eine Volltextsuche über mehrere Felder.
add_db_fld($server, $table, $field)
Fügt ein neues Feld zu einer bestehenden Tabelle in der Datenbank hinzu.
rollback(string $dd)
Führt ein Rollback für die Datenbank der angegebenen DD aus.
get_dd_table($dd, $rec=0)
Gibt den Tabellennamen oder die komplette Table-Definition einer DD zurück.
check_access(string $mode, string $dd)
Prüft die Zugriffsrechte für eine Operation auf einer DD.
check_fields($dd, $field_values)
Filtert ungültige Felder aus einem Eingabearray heraus.
exec_query($server, $sql)
Führt eine generische SQL-Exec-Anweisung aus.
commit($dd)
Committet eine laufende Transaktion der Datenbank einer DD.
clear_db_error()
Setzt den letzten DB-Fehlerzustand zurueck.
__construct()
Initialisiert das DBX-Datenbankobjekt mit Standardwerten und lädt den zentralen Validator.
get_csv_seperator($dd)
Gibt das CSV-Trennzeichen für eine DD zurück.
get_insert_id()
Liefert die letzte Insert-ID nach erfolgreichem insert().
update($dd, $field_values, $where, $verify_access=1, $verify_fields=1, $verify_values=1, $trace=1)
Führt ein UPDATE-Statement auf einer Tabelle aus.
get_new_record(string $dd, array $field_values=[])
Erstellt einen neuen Datensatz mit Standardwerten.
get_rpt_fields($dd, $flds, $label=1)
Ermittelt die Report-Felder einer DD für eine übergebene Feldliste.
map_dd_type_to_grid_type(string $ddType)
Mappt DD-Feldtypen auf Grid-Feldtypen.
escape($string, $server)
Escaped einen String für die sichere Verwendung in einer SQL-Abfrage.
delete_query($server, $sql)
Führt eine DELETE-Abfrage aus und gibt die Anzahl der betroffenen Zeilen zurück.
get_dd_exist($dd)
Prüft, ob eine DD-Datei im Standardmodul dbx existiert.
get_dd_primary(string $dd)
Ermittelt den Primärschlüssel einer Datenbeschreibung (DD).
get_dd_server(string $dd)
Ermittelt den lokal wirksamen Server einer Datenbeschreibung.
$_validation_warning
isSQLiteDatabaseLocked($databasePath)
Prüft, ob eine SQLite-Datenbank aktuell gelockt ist.
get_db_type($server)
Ermittelt den Datenbanktyp für einen Server.
select_query(string $server, string $sql, string $dd='')
Fuehrt eine rohe SELECT-Abfrage auf einem konfigurierten DB-Server aus.
get_db_tables($server, $not='sqlite_sequence')
Liefert alle Tabellen eines Servers mit Datensatzanzahl zurück.
select1(string $dd, $where='', $columns=' *', $verify_access=1)
Führt eine SELECT-Abfrage aus und gibt genau einen Datensatz zurück.
get_dd_fields($dd, $label=0)
Gibt die Felddefinitionen einer DD zurück.
check_where($where, $owner=0, $dd='')
get_convert_array($field, $array, $convert='auto')
Konvertiert Array-Werte automatisch in String-/Serialisierungsformate.
get_dd_server_bindings()
Liefert die installationsbezogenen Serverbindungen der DDs.
connect_db_server(string $server)
Verbindet sich mit einem angegebenen Datenbankserver.
get_dd_sort_flds($dd)
Platzhalter für Sortierfelder einer DD.
$_validatior_type
if((int)( $admin[ 'id'] ?? 0)<=0) if(!dbx() ->patch_local_config('dbxLogin', array('register'=> '0'))) $binding
user($key='id')
Liest einen Wert des aktuellen Benutzers.
Definition dbxApi.php:1305
get_base_dir(int $cutData=0)
Liefert das Basisverzeichnis der Installation.
Definition dbxApi.php:1507
dbx_lng_current()
Lädt eine Klasse aus dem Cache oder erstellt eine neue Instanz der Klasse.
Definition dbxApi.php:3038
can($access_groups='', $user_groups='')
Prueft Gruppenrechte gegen den aktuellen oder uebergebenen Benutzer.
Definition dbxApi.php:1381
is_int_value($value)
Prueft, ob ein Wert als Integer verwendbar ist.
Definition dbxApi.php:2435
if($stored !=='files/test/') $resolved
if($resolved !==$expectedBase . 'files/test/') $config
foreach($heroTemplates as $templateFile) $sections
if(!empty( $validation[ 'errors'])) if($service->isAllowedDesignFile('../escape.php')|| $service->isAllowedDesignFile('htm/evil.php')) $delta
if(array_key_exists('install', $otherModule)) $missingFile
if(!defined( 'IMG_WEBP')) define( 'IMG_WEBP'
if( $demoId<=0) foreach(array('create_date', 'create_uid', 'update_date', 'update_uid', 'owner',) as $systemField) $items
DBX schema administration.