dolibarr 25.0.0-alpha
mysqli.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2001 Fabien Seisen <seisen@linuxfr.org>
3 * Copyright (C) 2002-2005 Rodolphe Quiedeville <rodolphe@quiedeville.org>
4 * Copyright (C) 2004-2011 Laurent Destailleur <eldy@users.sourceforge.net>
5 * Copyright (C) 2006 Andre Cianfarani <acianfa@free.fr>
6 * Copyright (C) 2005-2012 Regis Houssin <regis.houssin@inodbox.com>
7 * Copyright (C) 2015 Raphaël Doursenaud <rdoursenaud@gpcsolutions.fr>
8 * Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
9 * Copyright (C) 2024 Charlene Benke <charlene@patas-monkey.com>
10 * Copyright (C) 2024-2026 Frédéric France <frederic.france@free.fr>
11 *
12 * This program is free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 3 of the License, or
15 * (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program. If not, see <https://www.gnu.org/licenses/>.
24 */
25
31require_once DOL_DOCUMENT_ROOT.'/core/db/DoliDB.class.php';
32
36class DoliDBMysqli extends DoliDB
37{
39 public $db;
41 public $type = 'mysqli';
42
44 const LABEL = 'MySQL or MariaDB';
46 const VERSIONMIN = '5.0.3';
47
49 private $_results;
50
63 public function __construct($type, $host, $user, $pass, $name = '', $port = 0, $forcenew = false) // @phpstan-ignore constructor.unusedParameter, constructor.unusedParameter
64 {
65 global $conf, $langs;
66
67 // Note that having "static" property for "$forcecharset" and "$forcecollate" will make error here in strict mode, so they are not static
68 if (!empty($conf->db->character_set)) {
69 $this->forcecharset = $conf->db->character_set;
70 }
71 if (!empty($conf->db->dolibarr_main_db_collation)) {
72 $this->forcecollate = $conf->db->dolibarr_main_db_collation;
73 }
74
75 $this->database_user = $user;
76 $this->database_host = $host;
77 $this->database_port = $port;
78
79 $this->transaction_opened = 0;
80
81 //print "Name DB: $host,$user,$pass,$name<br>";
82
83 if (!class_exists('mysqli')) {
84 $this->connected = false;
85 $this->ok = false;
86 $this->error = "Mysqli PHP functions for using Mysqli driver are not available in this version of PHP. Try to use another driver.";
87 dol_syslog(get_class($this)."::DoliDBMysqli : Mysqli PHP functions for using Mysqli driver are not available in this version of PHP. Try to use another driver.", LOG_ERR);
88 }
89
90 if (!$host) {
91 $this->connected = false;
92 $this->ok = false;
93 $this->error = $langs->trans("ErrorWrongHostParameter");
94 dol_syslog(get_class($this)."::DoliDBMysqli : Connect error, wrong host parameters", LOG_ERR);
95 }
96
97 // Try server connection
98 // We do not try to connect to database, only to server. Connect to database is done later in constructor
99 $this->db = $this->connect($host, $user, $pass, '', $port);
100
101 if ($this->db && empty($this->db->connect_errno)) {
102 $this->connected = true;
103 $this->ok = true;
104 } else {
105 $this->connected = false;
106 $this->ok = false;
107 $this->error = empty($this->db) ? 'Failed to connect' : $this->db->connect_error;
108 dol_syslog(get_class($this)."::DoliDBMysqli Connect error: ".$this->error, LOG_ERR);
109 }
110
111 $disableforcecharset = 0; // Set to 1 to test without charset forcing
112
113 // If server connection is ok, we try to connect to the database
114 if ($this->connected && $name) {
115 if ($this->select_db($name)) {
116 $this->database_selected = true;
117 $this->database_name = $name;
118 $this->ok = true;
119
120 // If client is old latin, we force utf8
121 $clientmustbe = empty($conf->db->character_set) ? 'utf8' : (string) $conf->db->character_set;
122 if (preg_match('/latin1/', $clientmustbe)) {
123 $clientmustbe = 'utf8';
124 }
125
126 if (empty($disableforcecharset) && $this->db->character_set_name() != $clientmustbe) {
127 try {
128 dol_syslog(get_class($this)."::DoliDBMysqli You should set the \$dolibarr_main_db_character_set and \$dolibarr_main_db_collation for the PHP to the same as the database default, so to ".$this->db->character_set_name(). " or upgrade database default to ".$clientmustbe.".", LOG_WARNING);
129 // To get current charset: USE databasename; SHOW VARIABLES LIKE 'character_set_database'
130 // or: USE databasename; SELECT schema_name, default_character_set_name FROM information_schema.SCHEMATA;
131 // To get current collation: USE databasename; SHOW VARIABLES LIKE 'collation_database'
132 // or: USE databasename; SELECT schema_name, default_character_set_name FROM information_schema.SCHEMATA;
133 // To upgrade database default, you can do: ALTER DATABASE databasename CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
134
135 $this->db->set_charset($clientmustbe); // This set charset, but with a bad collation (colllation is forced later)
136 } catch (Throwable $e) {
137 print 'Failed to force character_set_client to '.$clientmustbe." (according to setup) to match the one of the server database.<br>\n";
138 print $e->getMessage();
139 print "<br>\n";
140 if ($clientmustbe != 'utf8') {
141 print 'Edit conf/conf.php file to set a charset "utf8"';
142 if ($clientmustbe != 'utf8mb4') {
143 print ' or "utf8mb4"';
144 }
145 print ' instead of "'.$clientmustbe.'".'."\n";
146 }
147 exit;
148 }
149
150 $collation = (empty($conf) ? 'utf8_unicode_ci' : (string) $conf->db->dolibarr_main_db_collation);
151 if (preg_match('/latin1/', $collation)) {
152 $collation = 'utf8_unicode_ci';
153 }
154
155 if (!preg_match('/general/', $collation)) {
156 $this->db->query("SET collation_connection = ".$collation);
157 }
158 }
159 } else {
160 $this->database_selected = false;
161 $this->database_name = '';
162 $this->ok = false;
163 $this->error = $this->error();
164 dol_syslog(get_class($this)."::DoliDBMysqli : Select_db error ".$this->error, LOG_ERR);
165 }
166 } else {
167 // No selection of database done. We may only be connected or not (ok or ko) to the server.
168 $this->database_selected = false;
169
170 if ($this->connected) {
171 // If client is old latin, we force utf8
172 $clientmustbe = empty($conf->db->character_set) ? 'utf8' : (string) $conf->db->character_set;
173 if (preg_match('/latin1/', $clientmustbe)) {
174 $clientmustbe = 'utf8';
175 }
176
177 if (empty($disableforcecharset) && $this->db->character_set_name() != $clientmustbe) {
178 $this->db->set_charset($clientmustbe); // This set utf8_unicode_ci or utf8mb4_unicode_ci
179
180 $collation = (string) $conf->db->dolibarr_main_db_collation;
181 if (preg_match('/latin1/', $collation)) {
182 $collation = 'utf8_unicode_ci';
183 }
184
185 if (!preg_match('/general/', $collation)) {
186 $this->db->query("SET collation_connection = ".$collation);
187 }
188 }
189 }
190 }
191 }
192
193
201 public function hintindex($nameofindex, $mode = 1)
202 {
203 return " ".($mode == 1 ? 'FORCE' : 'USE')." INDEX(".preg_replace('/[^a-z0-9_]/', '', $nameofindex).")";
204 }
205
206
214 public function convertSQLFromMysql($line, $type = 'ddl')
215 {
216 return $line;
217 }
218
219
220 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
227 public function select_db($database)
228 {
229 // phpcs:enable
230 dol_syslog(get_class($this)."::select_db database=".$database, LOG_DEBUG);
231 $result = false;
232 try {
233 $result = $this->db->select_db($database);
234 } catch (Throwable $e) {
235 // Nothing done on error
236 }
237 return $result;
238 }
239
240
253 public function connect($host, $login, $passwd, $name, $port = 0, $forcenew = false)
254 {
255 dol_syslog(get_class($this)."::connect host=$host, port=$port, login=$login, passwd=--hidden--, name=$name", LOG_DEBUG);
256
257 //mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
258
259 $tmp = false;
260 try {
261 if (!class_exists('mysqli')) {
262 dol_print_error(null, 'Driver mysqli for PHP not available');
263 return false;
264 }
265 if (strpos($host, 'ssl://') === 0) {
266 $tmp = new mysqliDoli($host, $login, $passwd, $name, $port);
267 } else {
268 $tmp = new mysqli($host, $login, $passwd, $name, $port);
269 }
270 } catch (Throwable $e) {
271 dol_syslog(get_class($this)."::connect failed", LOG_DEBUG);
272 }
273 return $tmp;
274 }
275
281 public function getVersion()
282 {
283 return $this->db->server_info;
284 }
285
291 public function getDriverInfo()
292 {
293 return $this->db->client_info;
294 }
295
296
303 public function close()
304 {
305 if ($this->db) {
306 if ($this->transaction_opened > 0) {
307 dol_syslog(get_class($this)."::close Closing a connection with an opened transaction depth=".$this->transaction_opened, LOG_ERR);
308 }
309 $this->connected = false;
310 return $this->db->close();
311 }
312 return false;
313 }
314
315
316
327 public function query($query, $usesavepoint = 0, $type = 'auto', $result_mode = 0)
328 {
329 global $dolibarr_main_db_readonly;
330
331 $query = trim($query);
332
333
334 /*if ($usesavepoint && $this->transaction_opened) {
335 dol_syslog(get_class($this)."::query SAVEPOINT mysavepoint", LOG_DEBUG); // Log of request was not yet done previously
336 $this->db->query('SAVEPOINT mysavepoint');
337 }*/
338
339 if (!in_array($query, array('BEGIN', 'COMMIT', 'ROLLBACK'))) {
340 $SYSLOG_SQL_LIMIT = 10000; // limit log to 10kb per line to limit DOS attacks
341 dol_syslog('sql='.substr($query, 0, $SYSLOG_SQL_LIMIT), LOG_DEBUG);
342 }
343 if (empty($query)) {
344 return false; // Return false = error if empty request
345 }
346
347 if (!empty($dolibarr_main_db_readonly)) {
348 if (preg_match('/^(INSERT|UPDATE|REPLACE|DELETE|CREATE|ALTER|TRUNCATE|DROP)/i', $query)) {
349 $this->lasterror = 'Application in read-only mode';
350 $this->lasterrno = 'APPREADONLY';
351 $this->lastquery = $query;
352 return false;
353 }
354 }
355
356 try {
357 $ret = $this->db->query($query, $result_mode);
358 } catch (Throwable $e) {
359 dol_syslog(get_class($this)."::query Exception in query instead of returning an error: ".$e->getMessage(), LOG_ERR);
360 $ret = false;
361 }
362
363 if (!preg_match("/^COMMIT/i", $query) && !preg_match("/^ROLLBACK/i", $query)) {
364 // If user query, we save it along with its resultset
365 if (!$ret) {
366 $this->lastqueryerror = $query;
367 $this->lasterror = $this->error();
368 $this->lasterrno = $this->errno();
369
370 if (getDolGlobalInt('SYSLOG_LEVEL') < LOG_DEBUG) {
371 dol_syslog(get_class($this)."::query SQL Error query: ".$query, LOG_ERR); // Log of request was not yet done previously
372 }
373 dol_syslog(get_class($this)."::query SQL Error message: ".$this->lasterrno." ".$this->lasterror.self::getCallerInfoString(), LOG_ERR);
374 //var_dump(debug_print_backtrace());
375 }
376
377 /*if ($usesavepoint && $this->transaction_opened) { // Warning, after that errno will be erased
378 dol_syslog(get_class($this)."::query ROLLBACK TO SAVEPOINT mysavepoint", LOG_DEBUG); // Log of request was not yet done previously
379 $this->db->query('ROLLBACK TO SAVEPOINT mysavepoint');
380 }*/
381
382 $this->lastquery = $query;
383 $this->_results = $ret;
384 }
385
386 return $ret;
387 }
388
394 final protected static function getCallerInfoString()
395 {
396 $backtrace = debug_backtrace();
397 $msg = "";
398 if (count($backtrace) >= 1) {
399 $trace = $backtrace[1];
400 if (isset($trace['file'], $trace['line'])) {
401 $msg = " From {$trace['file']}:{$trace['line']}.";
402 }
403 }
404 return $msg;
405 }
406
407 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
414 public function fetch_object($resultset)
415 {
416 // phpcs:enable
417 // If the resultset was not provided, we get the last one for this connection
418 if (!is_object($resultset)) {
419 $resultset = $this->_results;
420 }
421 return $resultset->fetch_object();
422 }
423
424
425 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
432 public function fetch_array($resultset)
433 {
434 // phpcs:enable
435 // If resultset not provided, we take the last used by connection
436 if (!is_object($resultset)) {
437 $resultset = $this->_results;
438 }
439 return $resultset->fetch_array();
440 }
441
442 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
449 public function fetch_row($resultset)
450 {
451 // phpcs:enable
452 // If resultset not provided, we take the last used by connection
453 if (!is_bool($resultset)) {
454 if (!is_object($resultset)) {
455 $resultset = $this->_results;
456 }
457 return $resultset->fetch_row();
458 } else {
459 // If the cursor is a boolean, return 0
460 return 0;
461 }
462 }
463
464 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
472 public function num_rows($resultset)
473 {
474 // phpcs:enable
475 // If resultset not provided, we take the last used by connection
476 if (!is_object($resultset)) {
477 $resultset = $this->_results;
478 }
479 return isset($resultset->num_rows) ? $resultset->num_rows : 0;
480 }
481
482 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
490 public function affected_rows($resultset)
491 {
492 // phpcs:enable
493 // If resultset not provided, we take the last used by connection
494 if (!is_object($resultset)) {
495 $resultset = $this->_results;
496 }
497 // mysql require a db link, not like pqsql that takes a resultset
498 return $this->db->affected_rows;
499 }
500
507 public function free($resultset = null)
508 {
509 // If resultset not provided, we take the last used by connection
510 if (!is_object($resultset)) {
511 $resultset = $this->_results;
512 }
513 // If resultset is provided, free memory
514 if (is_object($resultset)) {
515 $resultset->free_result();
516 }
517 }
518
525 public function escape($stringtoencode)
526 {
527 return $this->db->real_escape_string((string) $stringtoencode);
528 }
529
536 public function escapeforlike($stringtoencode)
537 {
538 // We must first replace the \ char into \\, then we can replace _ and % into \_ and \%
539 return str_replace(array('\\', '_', '%'), array('\\\\', '\_', '\%'), (string) $stringtoencode);
540 }
541
547 public function errno()
548 {
549 if (!$this->connected) {
550 // If the connection failed, $this->db is not valid.
551 return 'DB_ERROR_FAILED_TO_CONNECT';
552 } else {
553 // Constants to convert a MySql error code to a generic Dolibarr error code
554 $errorcode_map = array(
555 1004 => 'DB_ERROR_CANNOT_CREATE',
556 1005 => 'DB_ERROR_CANNOT_CREATE',
557 1006 => 'DB_ERROR_CANNOT_CREATE',
558 1007 => 'DB_ERROR_ALREADY_EXISTS',
559 1008 => 'DB_ERROR_CANNOT_DROP',
560 1022 => 'DB_ERROR_KEY_NAME_ALREADY_EXISTS',
561 1025 => 'DB_ERROR_NO_FOREIGN_KEY_TO_DROP',
562 1044 => 'DB_ERROR_ACCESSDENIED',
563 1046 => 'DB_ERROR_NODBSELECTED',
564 1048 => 'DB_ERROR_CONSTRAINT',
565 1050 => 'DB_ERROR_TABLE_ALREADY_EXISTS',
566 1051 => 'DB_ERROR_NOSUCHTABLE',
567 1054 => 'DB_ERROR_NOSUCHFIELD',
568 1060 => 'DB_ERROR_COLUMN_ALREADY_EXISTS',
569 1061 => 'DB_ERROR_KEY_NAME_ALREADY_EXISTS',
570 1062 => 'DB_ERROR_RECORD_ALREADY_EXISTS',
571 1064 => 'DB_ERROR_SYNTAX',
572 1068 => 'DB_ERROR_PRIMARY_KEY_ALREADY_EXISTS',
573 1075 => 'DB_ERROR_CANT_DROP_PRIMARY_KEY',
574 1091 => 'DB_ERROR_NOSUCHFIELD',
575 1100 => 'DB_ERROR_NOT_LOCKED',
576 1136 => 'DB_ERROR_VALUE_COUNT_ON_ROW',
577 1146 => 'DB_ERROR_NOSUCHTABLE',
578 1215 => 'DB_ERROR_CANNOT_ADD_FOREIGN_KEY_CONSTRAINT',
579 1216 => 'DB_ERROR_NO_PARENT',
580 1217 => 'DB_ERROR_CHILD_EXISTS',
581 1396 => 'DB_ERROR_USER_ALREADY_EXISTS', // When creating a user that already existing
582 1451 => 'DB_ERROR_CHILD_EXISTS',
583 1824 => 'DB_ERROR_CANNOT_CREATE', // When creating a constraint on a parent table that does not exists
584 1826 => 'DB_ERROR_KEY_NAME_ALREADY_EXISTS'
585 );
586
587 if (isset($errorcode_map[$this->db->errno])) {
588 return $errorcode_map[$this->db->errno];
589 }
590 $errno = $this->db->errno;
591 return ($errno ? 'DB_ERROR_'.$errno : '0');
592 }
593 }
594
600 public function error()
601 {
602 if (!$this->connected) {
603 // When there is a connection failure, $this->db is invalid for to get mysqli_error.
604 return 'Not connected. Check setup parameters in conf/conf.php file and your mysql client and server versions';
605 } else {
606 return $this->db->error;
607 }
608 }
609
610 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
618 public function last_insert_id($tab, $fieldid = 'rowid')
619 {
620 // phpcs:enable
621 return $this->db->insert_id;
622 }
623
632 public function encrypt($fieldorvalue, $withQuotes = 1)
633 {
634 global $conf;
635
636 // Type of encryption (2: AES (recommended), 1: DES , 0: no encryption)
637 $cryptType = (!empty($conf->db->dolibarr_main_db_encryption) ? $conf->db->dolibarr_main_db_encryption : 0);
638
639 //Encryption key
640 $cryptKey = (!empty($conf->db->dolibarr_main_db_cryptkey) ? $conf->db->dolibarr_main_db_cryptkey : '');
641
642 $escapedstringwithquotes = ($withQuotes ? "'" : "").$this->escape($fieldorvalue).($withQuotes ? "'" : "");
643
644 if ($cryptType && !empty($cryptKey)) {
645 if ($cryptType == 2) {
646 $escapedstringwithquotes = "AES_ENCRYPT(".$escapedstringwithquotes.", '".$this->escape($cryptKey)."')";
647 } elseif ($cryptType == 1) {
648 $escapedstringwithquotes = "DES_ENCRYPT(".$escapedstringwithquotes.", '".$this->escape($cryptKey)."')";
649 }
650 }
651
652 return $escapedstringwithquotes;
653 }
654
661 public function decrypt($value)
662 {
663 global $conf;
664
665 // Type of encryption (2: AES (recommended), 1: DES , 0: no encryption)
666 $cryptType = (!empty($conf->db->dolibarr_main_db_encryption) ? $conf->db->dolibarr_main_db_encryption : 0);
667
668 //Encryption key
669 $cryptKey = (!empty($conf->db->dolibarr_main_db_cryptkey) ? $conf->db->dolibarr_main_db_cryptkey : '');
670
671 $return = $value;
672
673 if ($cryptType && !empty($cryptKey)) {
674 if ($cryptType == 2) {
675 $return = 'AES_DECRYPT('.$value.',\''.$cryptKey.'\')';
676 } elseif ($cryptType == 1) {
677 $return = 'DES_DECRYPT('.$value.',\''.$cryptKey.'\')';
678 }
679 }
680
681 return $return;
682 }
683
684
685 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
691 public function DDLGetConnectId()
692 {
693 // phpcs:enable
694 $resql = $this->query('SELECT CONNECTION_ID()');
695 if ($resql) {
696 $row = $this->fetch_row($resql);
697 return $row[0];
698 } else {
699 return '?';
700 }
701 }
702
703 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
715 public function DDLCreateDb($database, $charset = '', $collation = '', $owner = '')
716 {
717 // phpcs:enable
718 if (empty($charset)) {
719 $charset = $this->forcecharset;
720 }
721 if (empty($collation)) {
722 $collation = $this->forcecollate;
723 }
724
725 // ALTER DATABASE dolibarr_db DEFAULT CHARACTER SET latin DEFAULT COLLATE latin1_swedish_ci
726 $sql = "CREATE DATABASE `".$this->sanitize($database)."`";
727 $sql .= " DEFAULT CHARACTER SET `".$this->sanitize($charset)."` DEFAULT COLLATE `".$this->sanitize($collation)."`";
728
729 dol_syslog($sql, LOG_DEBUG);
730 $ret = $this->query($sql);
731 if (!$ret) {
732 // We try again for compatibility with Mysql < 4.1.1
733 $sql = "CREATE DATABASE `".$this->sanitize($database)."`";
734 dol_syslog($sql, LOG_DEBUG);
735 $ret = $this->query($sql);
736 }
737
738 return $ret;
739 }
740
741 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
749 public function DDLListTables($database, $table = '')
750 {
751 // phpcs:enable
752 $listtables = array();
753
754 $like = '';
755 if ($table) {
756 $tmptable = preg_replace('/[^a-z0-9\.\-\_%]/i', '', $table);
757
758 $like = "LIKE '".$this->escape($tmptable)."'";
759 }
760 $tmpdatabase = preg_replace('/[^a-z0-9\.\-\_]/i', '', $database);
761
762 $sql = "SHOW TABLES FROM `".$tmpdatabase."` ".$like.";";
763 //print $sql;
764 $result = $this->query($sql);
765 if ($result) {
766 while ($row = $this->fetch_row($result)) {
767 $listtables[] = $row[0];
768 }
769 }
770 return $listtables;
771 }
772
773 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
781 public function DDLListTablesFull($database, $table = '')
782 {
783 // phpcs:enable
784 $listtables = array();
785
786 $like = '';
787 if ($table) {
788 $tmptable = preg_replace('/[^a-z0-9\.\-\_%]/i', '', $table);
789
790 $like = "LIKE '".$this->escape($tmptable)."'";
791 }
792 $tmpdatabase = preg_replace('/[^a-z0-9\.\-\_]/i', '', $database);
793
794 $sql = "SHOW FULL TABLES FROM `".$tmpdatabase."` ".$like.";";
795
796 $result = $this->query($sql);
797 if ($result) {
798 while ($row = $this->fetch_row($result)) {
799 $listtables[] = $row;
800 }
801 }
802 return $listtables;
803 }
804
805 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
812 public function DDLInfoTable($table)
813 {
814 // phpcs:enable
815 $infotables = array();
816
817 $sanitizedtmptable = preg_replace('/[^a-z0-9\.\-\_]/i', '', $table);
818
819 $sql = "SHOW FULL COLUMNS FROM ".$sanitizedtmptable.";";
820
821 dol_syslog($sql, LOG_DEBUG);
822 $result = $this->query($sql);
823 if ($result) {
824 while ($row = $this->fetch_row($result)) {
825 $infotables[] = $row;
826 }
827 }
828 return $infotables;
829 }
830
831 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
844 public function DDLCreateTable($table, $fields, $primary_key, $type, $unique_keys = null, $fulltext_keys = null, $keys = null)
845 {
846 // phpcs:enable
847 // @TODO: $fulltext_keys parameter is unused
848
849 if (empty($type)) {
850 $type = 'InnoDB';
851 }
852
853 $pk = '';
854 $sqlk = array();
855 $sqluq = array();
856
857 // Keys found into the array $fields: type,value,attribute,null,default,extra
858 // ex. : $fields['rowid'] = array(
859 // 'type'=>'int' or 'integer',
860 // 'value'=>'11',
861 // 'null'=>'not null',
862 // 'extra'=> 'auto_increment'
863 // );
864 $sql = "CREATE TABLE ".$this->sanitize($table)."(";
865 $i = 0;
866 $sqlfields = array();
867 foreach ($fields as $field_name => $field_desc) {
868 $sqlfields[$i] = $this->sanitize($field_name)." ";
869 $sqlfields[$i] .= $this->sanitize($field_desc['type']);
870 if (isset($field_desc['value']) && $field_desc['value'] !== '') {
871 $sqlfields[$i] .= "(".$this->sanitize($field_desc['value']).")";
872 }
873 if (isset($field_desc['attribute']) && $field_desc['attribute'] !== '') {
874 $sqlfields[$i] .= " ".$this->sanitize($field_desc['attribute'], 0, 0, 1); // Allow space to accept attributes like "ON UPDATE CURRENT_TIMESTAMP"
875 }
876 if (isset($field_desc['default']) && $field_desc['default'] !== '') {
877 if (in_array($field_desc['type'], array('tinyint', 'smallint', 'int', 'double'))) {
878 $sqlfields[$i] .= " DEFAULT ".((float) $field_desc['default']);
879 } elseif ($field_desc['default'] == 'null' || $field_desc['default'] == 'CURRENT_TIMESTAMP') {
880 $sqlfields[$i] .= " DEFAULT ".$this->sanitize($field_desc['default']);
881 } else {
882 $sqlfields[$i] .= " DEFAULT '".$this->escape($field_desc['default'])."'";
883 }
884 }
885 if (isset($field_desc['null']) && $field_desc['null'] !== '') {
886 $sqlfields[$i] .= " ".$this->sanitize($field_desc['null'], 0, 0, 1);
887 }
888 if (isset($field_desc['extra']) && $field_desc['extra'] !== '') {
889 $sqlfields[$i] .= " ".$this->sanitize($field_desc['extra'], 0, 0, 1);
890 }
891 if (!empty($primary_key) && $primary_key == $field_name) {
892 $sqlfields[$i] .= " AUTO_INCREMENT PRIMARY KEY"; // mysql instruction that will be converted by driver late
893 }
894 $i++;
895 }
896
897 if (is_array($unique_keys)) {
898 $i = 0;
899 foreach ($unique_keys as $key => $value) {
900 $sqluq[$i] = "UNIQUE KEY '".$this->sanitize($key)."' ('".$this->escape($value)."')";
901 $i++;
902 }
903 }
904 if (is_array($keys)) {
905 $i = 0;
906 foreach ($keys as $key => $value) {
907 $sqlk[$i] = "KEY ".$this->sanitize($key)." (".$value.")";
908 $i++;
909 }
910 }
911 $sql .= implode(', ', $sqlfields);
912 if (!is_array($unique_keys) && $unique_keys != "") {
913 $sql .= ",".implode(',', $sqluq);
914 }
915 if (is_array($keys)) {
916 $sql .= ",".implode(',', $sqlk);
917 }
918 $sql .= ")";
919 $sql .= " engine=".$this->sanitize($type);
920
921 if (!$this->query($sql)) {
922 return -1;
923 } else {
924 return 1;
925 }
926 }
927
928 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
935 public function DDLDropTable($table)
936 {
937 // phpcs:enable
938 $tmptable = preg_replace('/[^a-z0-9\.\-\_]/i', '', $table);
939
940 $sql = "DROP TABLE ".$this->sanitize($tmptable);
941
942 if (!$this->query($sql)) {
943 return -1;
944 } else {
945 return 1;
946 }
947 }
948
949 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
957 public function DDLDescTable($table, $field = "")
958 {
959 // phpcs:enable
960 $sql = "DESC ".$this->sanitize($table)." ".$this->sanitize($field);
961
962 dol_syslog(get_class($this)."::DDLDescTable ".$sql, LOG_DEBUG);
963 $this->_results = $this->query($sql);
964 return $this->_results;
965 }
966
967 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
977 public function DDLAddField($table, $field_name, $field_desc, $field_position = "")
978 {
979 // phpcs:enable
980 // keys looked up in the descriptions array (field_desc): type,value,attribute,null,default,extra
981 // ex. : $field_desc = array('type'=>'int','value'=>'11','null'=>'not null','extra'=> 'auto_increment');
982 $sql = "ALTER TABLE ".$this->sanitize($table)." ADD ".$this->sanitize($field_name)." ";
983
984 if ($field_desc['type'] !== 'datetimegmt') {
985 $sql .= $this->sanitize($field_desc['type']);
986 } else {
987 $sql .= 'datetime';
988 }
989
990 if (in_array($field_desc['type'], array('double', 'int', 'varchar')) && array_key_exists('value', $field_desc) && !empty($field_desc['value'])) {
991 $sql .= "(".$this->sanitize($field_desc['value']).")";
992 }
993 if (isset($field_desc['attribute']) && preg_match("/^[^\s]/i", $field_desc['attribute'])) {
994 $sql .= " ".$this->sanitize($field_desc['attribute']);
995 }
996 if (isset($field_desc['null']) && preg_match("/^[^\s]/i", $field_desc['null'])) {
997 if ($field_desc['null'] == 'NOT NULL') {
998 $sql .= " ".$this->sanitize($field_desc['null'], 0, 0, 1);
999 } else {
1000 $sql .= " ".$this->sanitize($field_desc['null']);
1001 }
1002 }
1003 if (isset($field_desc['default']) && preg_match("/^[^\s]/i", $field_desc['default'])) {
1004 if (in_array($field_desc['type'], array('tinyint', 'smallint', 'int', 'double'))) {
1005 $sql .= " DEFAULT ".((float) $field_desc['default']);
1006 } elseif ($field_desc['default'] == 'null' || $field_desc['default'] == 'CURRENT_TIMESTAMP') {
1007 $sql .= " DEFAULT ".$this->sanitize($field_desc['default']);
1008 } else {
1009 $sql .= " DEFAULT '".$this->escape($field_desc['default'])."'";
1010 }
1011 }
1012 if (isset($field_desc['extra']) && preg_match("/^[^\s]/i", $field_desc['extra'])) {
1013 $sql .= " ".$this->sanitize($field_desc['extra'], 0, 0, 1);
1014 }
1015 $sql .= " ".$this->sanitize($field_position, 0, 0, 1);
1016
1017 dol_syslog(get_class($this)."::DDLAddField ".$sql, LOG_DEBUG);
1018 if ($this->query($sql)) {
1019 return 1;
1020 }
1021 // A table where columns were added and dropped many times refuses any new one with "Row size too
1022 // large" (error 1118), because the space the dropped columns took in the physical record is only
1023 // given back by a table rebuild. Rebuilding needs privileges the database user of the application
1024 // may not have, so we only tell the administrator which command recovers the table.
1025 if ($this->lasterrno == 'DB_ERROR_1118') {
1026 $hint = "Table ".$table." must be rebuilt by your database administrator with the command: ALTER TABLE ".$table." FORCE";
1027 dol_syslog(get_class($this)."::DDLAddField ".$hint, LOG_WARNING);
1028 $this->lasterror .= ' - '.$hint;
1029 }
1030 return -1;
1031 }
1032
1033 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1042 public function DDLUpdateField($table, $field_name, $field_desc)
1043 {
1044 // phpcs:enable
1045 $sql = "ALTER TABLE ".$this->sanitize($table);
1046 $sql .= " MODIFY COLUMN ".$this->sanitize($field_name)." ";
1047
1048 if ($field_desc['type'] !== 'datetimegmt') {
1049 $sql .= $this->sanitize($field_desc['type']);
1050 } else {
1051 $sql .= 'datetime';
1052 }
1053
1054 if (in_array($field_desc['type'], array('double', 'int', 'varchar')) && array_key_exists('value', $field_desc) && !empty($field_desc['value'])) {
1055 $sql .= "(".$this->sanitize($field_desc['value']).")";
1056 }
1057 if (isset($field_desc['null']) && ($field_desc['null'] == 'not null' || $field_desc['null'] == 'NOT NULL')) {
1058 // We will try to change format of column to NOT NULL. To be sure the ALTER works, we try to update fields that are NULL
1059 if ($field_desc['type'] == 'varchar' || $field_desc['type'] == 'text') {
1060 $sqlbis = "UPDATE ".$this->sanitize($table)." SET ".$this->sanitize($field_name)." = '".$this->escape(isset($field_desc['default']) ? $field_desc['default'] : '')."' WHERE ".$this->sanitize($field_name)." IS NULL";
1061 $this->query($sqlbis);
1062 } elseif (in_array($field_desc['type'], array('tinyint', 'smallint', 'int', 'double'))) {
1063 $sqlbis = "UPDATE ".$this->sanitize($table)." SET ".$this->sanitize($field_name)." = ".((float) $this->escape(isset($field_desc['default']) ? $field_desc['default'] : 0))." WHERE ".$this->sanitize($field_name)." IS NULL";
1064 $this->query($sqlbis);
1065 }
1066
1067 $sql .= " NOT NULL";
1068 }
1069
1070 if (isset($field_desc['default']) && $field_desc['default'] != '') {
1071 if (in_array($field_desc['type'], array('tinyint', 'smallint', 'int', 'double'))) {
1072 $sql .= " DEFAULT ".((float) $field_desc['default']);
1073 } elseif ($field_desc['type'] != 'text') {
1074 $sql .= " DEFAULT '".$this->escape($field_desc['default'])."'"; // Default not supported on text fields
1075 }
1076 }
1077
1078 //print $sql;exit;
1079 dol_syslog(get_class($this)."::DDLUpdateField ".$sql, LOG_DEBUG);
1080 if (!$this->query($sql)) {
1081 return -1;
1082 } else {
1083 return 1;
1084 }
1085 }
1086
1087 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1095 public function DDLDropField($table, $field_name)
1096 {
1097 // phpcs:enable
1098 $tmp_field_name = preg_replace('/[^a-z0-9\.\-\_]/i', '', $field_name);
1099
1100 $sql = "ALTER TABLE ".$this->sanitize($table)." DROP COLUMN `".$this->sanitize($tmp_field_name)."`";
1101 if ($this->query($sql)) {
1102 return 1;
1103 }
1104 $this->error = $this->lasterror();
1105 return -1;
1106 }
1107
1108
1109 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1119 public function DDLCreateUser($dolibarr_main_db_host, $dolibarr_main_db_user, $dolibarr_main_db_pass, $dolibarr_main_db_name)
1120 {
1121 // phpcs:enable
1122 $sql = "CREATE USER '".$this->escape($dolibarr_main_db_user)."' IDENTIFIED BY '".$this->escape($dolibarr_main_db_pass)."'";
1123 dol_syslog(get_class($this)."::DDLCreateUser", LOG_DEBUG); // No sql to avoid password in log
1124 $resql = $this->query($sql);
1125 if (!$resql) {
1126 if ($this->lasterrno != 'DB_ERROR_USER_ALREADY_EXISTS') {
1127 return -1;
1128 } else {
1129 // If user already exists, we continue to set permissions
1130 dol_syslog(get_class($this)."::DDLCreateUser sql=".$sql, LOG_WARNING);
1131 }
1132 }
1133
1134 // Redo with localhost forced (sometimes user is created on %)
1135 $sql = "CREATE USER '".$this->escape($dolibarr_main_db_user)."'@'localhost' IDENTIFIED BY '".$this->escape($dolibarr_main_db_pass)."'";
1136 $resql = $this->query($sql);
1137
1138 $sql = "GRANT ALL PRIVILEGES ON `".$this->sanitize($dolibarr_main_db_name)."`.* TO '".$this->escape($dolibarr_main_db_user)."'@'".$this->escape($dolibarr_main_db_host)."'";
1139 dol_syslog(get_class($this)."::DDLCreateUser", LOG_DEBUG); // No sql to avoid password in log
1140 $resql = $this->query($sql);
1141 if (!$resql) {
1142 $this->error = "Connected user not allowed to GRANT ALL PRIVILEGES ON ".$this->escape($dolibarr_main_db_name).".* TO '".$this->escape($dolibarr_main_db_user)."'@'".$this->escape($dolibarr_main_db_host)."'";
1143 return -1;
1144 }
1145
1146 $sql = "FLUSH Privileges";
1147
1148 dol_syslog(get_class($this)."::DDLCreateUser", LOG_DEBUG);
1149 $resql = $this->query($sql);
1150 if (!$resql) {
1151 return -1;
1152 }
1153
1154 return 1;
1155 }
1156
1164 public function getDefaultCharacterSetDatabase()
1165 {
1166 $resql = $this->query("SHOW VARIABLES LIKE 'character_set_database'");
1167 if (!$resql) {
1168 // version Mysql < 4.1.1
1169 return $this->forcecharset;
1170 }
1171 $liste = $this->fetch_array($resql);
1172 $tmpval = $liste['Value'];
1173
1174 return $tmpval;
1175 }
1176
1182 public function getListOfCharacterSet()
1183 {
1184 $resql = $this->query('SHOW CHARSET');
1185 $liste = array();
1186 if ($resql) {
1187 $i = 0;
1188 while ($obj = $this->fetch_object($resql)) {
1189 $liste[$i]['charset'] = $obj->Charset;
1190 $liste[$i]['description'] = $obj->Description;
1191 $i++;
1192 }
1193 $this->free($resql);
1194 } else {
1195 // version Mysql < 4.1.1
1196 return null;
1197 }
1198 return $liste;
1199 }
1200
1207 public function getDefaultCollationDatabase()
1208 {
1209 $resql = $this->query("SHOW VARIABLES LIKE 'collation_database'");
1210 if (!$resql) {
1211 // version Mysql < 4.1.1
1212 return $this->forcecollate;
1213 }
1214 $liste = $this->fetch_array($resql);
1215 $tmpval = $liste['Value'];
1216
1217 return $tmpval;
1218 }
1219
1225 public function getListOfCollation()
1226 {
1227 $resql = $this->query('SHOW COLLATION');
1228 $liste = array();
1229 if ($resql) {
1230 $i = 0;
1231 while ($obj = $this->fetch_object($resql)) {
1232 $liste[$i]['collation'] = $obj->Collation;
1233 $i++;
1234 }
1235 $this->free($resql);
1236 } else {
1237 // version Mysql < 4.1.1
1238 return null;
1239 }
1240 return $liste;
1241 }
1242
1248 public function getPathOfDump()
1249 {
1250 $fullpathofdump = '/pathtomysqldump/mysqldump';
1251
1252 $resql = $this->query("SHOW VARIABLES LIKE 'basedir'");
1253 if ($resql) {
1254 $liste = $this->fetch_array($resql);
1255 $basedir = $liste['Value'];
1256 $fullpathofdump = $basedir.(preg_match('/\/$/', $basedir) ? '' : '/').'bin/mysqldump';
1257 }
1258 return $fullpathofdump;
1259 }
1260
1266 public function getPathOfRestore()
1267 {
1268 $fullpathofimport = '/pathtomysql/mysql';
1269
1270 $resql = $this->query("SHOW VARIABLES LIKE 'basedir'");
1271 if ($resql) {
1272 $liste = $this->fetch_array($resql);
1273 $basedir = $liste['Value'];
1274 $fullpathofimport = $basedir.(preg_match('/\/$/', $basedir) ? '' : '/').'bin/mysql';
1275 }
1276 return $fullpathofimport;
1277 }
1278
1285 public function getServerParametersValues($filter = '')
1286 {
1287 $result = array();
1288
1289 $sql = 'SHOW VARIABLES';
1290 if ($filter) {
1291 $sql .= " LIKE '".$this->escape($filter)."'";
1292 }
1293 $resql = $this->query($sql);
1294 if ($resql) {
1295 while ($obj = $this->fetch_object($resql)) {
1296 $result[$obj->Variable_name] = $obj->Value;
1297 }
1298 }
1299
1300 return $result;
1301 }
1302
1309 public function getServerStatusValues($filter = '')
1310 {
1311 $result = array();
1312
1313 $sql = 'SHOW STATUS';
1314 if ($filter) {
1315 $sql .= " LIKE '".$this->escape($filter)."'";
1316 }
1317 $resql = $this->query($sql);
1318 if ($resql) {
1319 while ($obj = $this->fetch_object($resql)) {
1320 $result[$obj->Variable_name] = $obj->Value;
1321 }
1322 }
1323
1324 return $result;
1325 }
1326
1333 public function getNextAutoIncrementId($table)
1334 {
1335 // Request to get last status of table
1336 $sql = "SHOW TABLE STATUS LIKE '".$this->escape($table)."'";
1337 $result = $this->query($sql);
1338
1339 if ($result) {
1340 $obj = $this->fetch_object($result);
1341 if ($obj && isset($obj->Auto_increment)) {
1342 return (int) $obj->Auto_increment;
1343 }
1344 }
1345
1346 return -1;
1347 }
1348
1356 public function prepare($sql)
1357 {
1358 if (!$this->connected) {
1359 $this->lasterror = 'Not connected to database';
1360 return false;
1361 }
1362 dol_syslog(get_class($this)."::prepare sql=".$sql, LOG_DEBUG);
1363 try {
1364 $stmt = $this->db->prepare($sql);
1365 } catch (Throwable $e) {
1366 // With MYSQLI_REPORT_STRICT, mysqli::prepare() throws instead of returning false
1367 $stmt = false;
1368 $this->lasterror = $e->getMessage();
1369 }
1370 if (!($stmt instanceof mysqli_stmt)) {
1371 if (empty($this->lasterror)) {
1372 $this->lasterror = $this->db->error;
1373 }
1374 $this->lastqueryerror = $sql;
1375 return false;
1376 }
1377
1378 return $stmt;
1379 }
1380
1390 public function execute($stmt, $params = array())
1391 {
1392 if (!($stmt instanceof mysqli_stmt)) {
1393 $this->lasterror = 'execute() called with an invalid statement';
1394 return false;
1395 }
1396
1397 $this->lasterror = '';
1398 $this->lastqueryerror = '';
1399
1400 $params = array_values($params);
1401 if (count($params) > 0) {
1402 $types = '';
1403 foreach ($params as $k => $v) {
1404 if (is_int($v) || is_bool($v)) {
1405 $types .= 'i';
1406 $params[$k] = (int) $v;
1407 } elseif (is_float($v)) {
1408 $types .= 'd';
1409 } else {
1410 $types .= 's'; // string; a null value is bound as NULL by mysqli
1411 }
1412 }
1413 // mysqli_stmt::bind_param() binds by reference, so pass references
1414 $bind = array($types);
1415 foreach ($params as $k => $v) {
1416 $bind[] = &$params[$k];
1417 }
1418 try {
1419 $ok = call_user_func_array(array($stmt, 'bind_param'), $bind);
1420 } catch (Throwable $e) {
1421 $ok = false;
1422 $this->lasterror = $e->getMessage();
1423 }
1424 if (!$ok) {
1425 if (empty($this->lasterror)) {
1426 $this->lasterror = $stmt->error;
1427 }
1428 $this->lastqueryerror = $stmt->sqlstate;
1429 return false;
1430 }
1431 }
1432
1433 dol_syslog(get_class($this)."::execute (".count($params)." bound param(s))", LOG_DEBUG);
1434
1435 try {
1436 $ok = $stmt->execute();
1437 } catch (Throwable $e) {
1438 // With MYSQLI_REPORT_STRICT, mysqli_stmt::execute() throws instead of returning false
1439 $ok = false;
1440 $this->lasterror = $e->getMessage();
1441 }
1442 if (!$ok) {
1443 if (empty($this->lasterror)) {
1444 $this->lasterror = $stmt->error;
1445 }
1446 $this->lastqueryerror = $stmt->sqlstate;
1447 return false;
1448 }
1449
1450 $res = $stmt->get_result();
1451 if ($res instanceof mysqli_result) {
1452 $this->_results = $res;
1453 return $res;
1454 }
1455
1456 // Not a result-returning statement (INSERT/UPDATE/DELETE...). affected_rows() reads
1457 // $this->db->affected_rows which mysqlnd updates after a prepared execute().
1458 return true;
1459 }
1460}
1461
1462if (class_exists('mysqli')) {
1466 class mysqliDoli extends mysqli
1467 {
1479 public function __construct($host, $user, $pass, $name, $port = 0, $socket = "") // @phpstan-ignore constructor.unusedParameter
1480 {
1481 $flags = 0;
1482 if (PHP_VERSION_ID >= 80100) {
1483 parent::__construct();
1484 } else {
1485 // @phan-suppress-next-line PhanDeprecatedFunctionInternal
1486 parent::init();
1487 }
1488 if (strpos($host, 'ssl://') === 0) {
1489 $host = substr($host, 6);
1490 parent::options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, 0);
1491 // Suppress false positive @phan-suppress-next-line PhanTypeMismatchArgumentInternalProbablyReal
1492 parent::ssl_set(null, null, "", null, null);
1493 $flags = MYSQLI_CLIENT_SSL;
1494 }
1495 parent::real_connect($host, $user, $pass, $name, $port, $socket, $flags);
1496 }
1497 }
1498}
$propal type
'integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter[:Sortfield]]]',...
Definition propal.php:280
Class to manage Dolibarr database access.
lastqueryerror()
Return last query in error.
lasterror()
Return last error label.
lasterrno()
Return last error code.
lastquery()
Return last request executed with query()
Class to manage Dolibarr database access for a MySQL database using the MySQLi extension.
fetch_array($resultset)
Return data as an array.
free($resultset=null)
Free the last pointer resultset used by this connection.
escapeforlike($stringtoencode)
Escape a string to insert data into a like.
num_rows($resultset)
Return number of lines for result of a SELECT.
hintindex($nameofindex, $mode=1)
Return SQL string to force an index.
const VERSIONMIN
Version min database.
$type
Database type.
error()
Return description of last error.
escape($stringtoencode)
Escape a string to insert data.
getVersion()
Return version of database server.
fetch_object($resultset)
Returns the current line (as an object) for the resultset cursor.
encrypt($fieldorvalue, $withQuotes=1)
Encrypt sensitive data in database Warning: This function includes the escape and add the SQL simple ...
__construct($type, $host, $user, $pass, $name='', $port=0, $forcenew=false)
Constructor.
convertSQLFromMysql($line, $type='ddl')
Convert a SQL request in Mysql syntax to native syntax.
affected_rows($resultset)
Return the number of lines in the result of a request INSERT, DELETE or UPDATE.
select_db($database)
Select a database.
decrypt($value)
Decrypt sensitive data in database.
fetch_row($resultset)
Return data as an array.
last_insert_id($tab, $fieldid='rowid')
Get last ID after an insert INSERT.
const LABEL
Database label.
query($query, $usesavepoint=0, $type='auto', $result_mode=0)
Execute a SQL request and return the resultset.
errno()
Return generic error code of last operation.
execute($stmt, $params=array())
Execute a statement previously created with prepare().
static getCallerInfoString()
Get caller info.
connect($host, $login, $passwd, $name, $port=0, $forcenew=false)
Connect to server.
getDriverInfo()
Return version of database client driver.
close()
Close database connection.
print $script_file $mode $langs defaultlang(is_numeric($duration_value) ? " delay=". $duration_value :"").(is_numeric($duration_value2) ? " after cd cd cd description as description
Only used if Module[ID]Desc translation string is not found.
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
if(!defined( 'CSRFCHECK_WITH_TOKEN'))