]> git.mxchange.org Git - friendica.git/blob - src/Database/Database.php
Merge pull request #7580 from annando/issue-7563
[friendica.git] / src / Database / Database.php
1 <?php
2
3 namespace Friendica\Database;
4
5 use Friendica\Core\Config\Cache\ConfigCache;
6 use Friendica\Core\System;
7 use Friendica\Network\HTTPException\InternalServerErrorException;
8 use Friendica\Util\DateTimeFormat;
9 use Friendica\Util\Profiler;
10 use mysqli;
11 use mysqli_result;
12 use mysqli_stmt;
13 use PDO;
14 use PDOException;
15 use PDOStatement;
16 use Psr\Log\LoggerInterface;
17
18 /**
19  * @class MySQL database class
20  *
21  * This class is for the low level database stuff that does driver specific things.
22  */
23 class Database
24 {
25         protected $connected = false;
26
27         /**
28          * @var ConfigCache
29          */
30         protected $configCache;
31         /**
32          * @var Profiler
33          */
34         protected $profiler;
35         /**
36          * @var LoggerInterface
37          */
38         protected $logger;
39         protected $server_info    = '';
40         /** @var PDO|mysqli */
41         protected $connection;
42         protected $driver;
43         private $error          = false;
44         private $errorno        = 0;
45         private $affected_rows  = 0;
46         protected $in_transaction = false;
47         protected $in_retrial     = false;
48         private $relation       = [];
49
50         public function __construct(ConfigCache $configCache, Profiler $profiler, LoggerInterface $logger, array $server = [])
51         {
52                 // We are storing these values for being able to perform a reconnect
53                 $this->configCache   = $configCache;
54                 $this->profiler      = $profiler;
55                 $this->logger        = $logger;
56
57                 $this->readServerVariables($server);
58                 $this->connect();
59
60                 if ($this->isConnected()) {
61                         // Loads DB_UPDATE_VERSION constant
62                         DBStructure::definition($configCache->get('system', 'basepath'), false);
63                 }
64         }
65
66         private function readServerVariables(array $server)
67         {
68                 // Use environment variables for mysql if they are set beforehand
69                 if (!empty($server['MYSQL_HOST'])
70                     && !empty($server['MYSQL_USERNAME'] || !empty($server['MYSQL_USER']))
71                     && $server['MYSQL_PASSWORD'] !== false
72                     && !empty($server['MYSQL_DATABASE']))
73                 {
74                         $db_host = $server['MYSQL_HOST'];
75                         if (!empty($server['MYSQL_PORT'])) {
76                                 $db_host .= ':' . $server['MYSQL_PORT'];
77                         }
78                         $this->configCache->set('database', 'hostname', $db_host);
79                         unset($db_host);
80                         if (!empty($server['MYSQL_USERNAME'])) {
81                                 $this->configCache->set('database', 'username', $server['MYSQL_USERNAME']);
82                         } else {
83                                 $this->configCache->set('database', 'username', $server['MYSQL_USER']);
84                         }
85                         $this->configCache->set('database', 'password', (string) $server['MYSQL_PASSWORD']);
86                         $this->configCache->set('database', 'database', $server['MYSQL_DATABASE']);
87                 }
88         }
89
90         public function connect()
91         {
92                 if (!is_null($this->connection) && $this->connected()) {
93                         return true;
94                 }
95
96                 $port       = 0;
97                 $serveraddr = trim($this->configCache->get('database', 'hostname'));
98                 $serverdata = explode(':', $serveraddr);
99                 $server     = $serverdata[0];
100                 if (count($serverdata) > 1) {
101                         $port = trim($serverdata[1]);
102                 }
103                 $server  = trim($server);
104                 $user    = trim($this->configCache->get('database', 'username'));
105                 $pass    = trim($this->configCache->get('database', 'password'));
106                 $db      = trim($this->configCache->get('database', 'database'));
107                 $charset = trim($this->configCache->get('database', 'charset'));
108
109                 if (!(strlen($server) && strlen($user))) {
110                         return false;
111                 }
112
113                 if (class_exists('\PDO') && in_array('mysql', PDO::getAvailableDrivers())) {
114                         $this->driver = 'pdo';
115                         $connect      = "mysql:host=" . $server . ";dbname=" . $db;
116
117                         if ($port > 0) {
118                                 $connect .= ";port=" . $port;
119                         }
120
121                         if ($charset) {
122                                 $connect .= ";charset=" . $charset;
123                         }
124
125                         try {
126                                 $this->connection = @new PDO($connect, $user, $pass);
127                                 $this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
128                                 $this->connected = true;
129                         } catch (PDOException $e) {
130                                 $this->connected = false;
131                         }
132                 }
133
134                 if (!$this->connected && class_exists('\mysqli')) {
135                         $this->driver = 'mysqli';
136
137                         if ($port > 0) {
138                                 $this->connection = @new mysqli($server, $user, $pass, $db, $port);
139                         } else {
140                                 $this->connection = @new mysqli($server, $user, $pass, $db);
141                         }
142
143                         if (!mysqli_connect_errno()) {
144                                 $this->connected = true;
145
146                                 if ($charset) {
147                                         $this->connection->set_charset($charset);
148                                 }
149                         }
150                 }
151
152                 // No suitable SQL driver was found.
153                 if (!$this->connected) {
154                         $this->driver     = null;
155                         $this->connection = null;
156                 }
157
158                 return $this->connected;
159         }
160
161         /**
162          * Sets the logger for DBA
163          *
164          * @note this is necessary because if we want to load the logger configuration
165          *       from the DB, but there's an error, we would print out an exception.
166          *       So the logger gets updated after the logger configuration can be retrieved
167          *       from the database
168          *
169          * @param LoggerInterface $logger
170          */
171         public function setLogger(LoggerInterface $logger)
172         {
173                 $this->logger = $logger;
174         }
175
176         /**
177          * Sets the profiler for DBA
178          *
179          * @param Profiler $profiler
180          */
181         public function setProfiler(Profiler $profiler)
182         {
183                 $this->profiler = $profiler;
184         }
185         /**
186          * Disconnects the current database connection
187          */
188         public function disconnect()
189         {
190                 if (is_null($this->connection)) {
191                         return;
192                 }
193
194                 switch ($this->driver) {
195                         case 'pdo':
196                                 $this->connection = null;
197                                 break;
198                         case 'mysqli':
199                                 $this->connection->close();
200                                 $this->connection = null;
201                                 break;
202                 }
203         }
204
205         /**
206          * Perform a reconnect of an existing database connection
207          */
208         public function reconnect()
209         {
210                 $this->disconnect();
211                 return $this->connect();
212         }
213
214         /**
215          * Return the database object.
216          *
217          * @return PDO|mysqli
218          */
219         public function getConnection()
220         {
221                 return $this->connection;
222         }
223
224         /**
225          * @brief Returns the MySQL server version string
226          *
227          * This function discriminate between the deprecated mysql API and the current
228          * object-oriented mysqli API. Example of returned string: 5.5.46-0+deb8u1
229          *
230          * @return string
231          */
232         public function serverInfo()
233         {
234                 if ($this->server_info == '') {
235                         switch ($this->driver) {
236                                 case 'pdo':
237                                         $this->server_info = $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION);
238                                         break;
239                                 case 'mysqli':
240                                         $this->server_info = $this->connection->server_info;
241                                         break;
242                         }
243                 }
244                 return $this->server_info;
245         }
246
247         /**
248          * @brief Returns the selected database name
249          *
250          * @return string
251          * @throws \Exception
252          */
253         public function databaseName()
254         {
255                 $ret  = $this->p("SELECT DATABASE() AS `db`");
256                 $data = $this->toArray($ret);
257                 return $data[0]['db'];
258         }
259
260         /**
261          * @brief Analyze a database query and log this if some conditions are met.
262          *
263          * @param string $query The database query that will be analyzed
264          *
265          * @throws \Exception
266          */
267         private function logIndex($query)
268         {
269
270                 if (!$this->configCache->get('system', 'db_log_index')) {
271                         return;
272                 }
273
274                 // Don't explain an explain statement
275                 if (strtolower(substr($query, 0, 7)) == "explain") {
276                         return;
277                 }
278
279                 // Only do the explain on "select", "update" and "delete"
280                 if (!in_array(strtolower(substr($query, 0, 6)), ["select", "update", "delete"])) {
281                         return;
282                 }
283
284                 $r = $this->p("EXPLAIN " . $query);
285                 if (!$this->isResult($r)) {
286                         return;
287                 }
288
289                 $watchlist = explode(',', $this->configCache->get('system', 'db_log_index_watch'));
290                 $blacklist = explode(',', $this->configCache->get('system', 'db_log_index_blacklist'));
291
292                 while ($row = $this->fetch($r)) {
293                         if ((intval($this->configCache->get('system', 'db_loglimit_index')) > 0)) {
294                                 $log = (in_array($row['key'], $watchlist) &&
295                                         ($row['rows'] >= intval($this->configCache->get('system', 'db_loglimit_index'))));
296                         } else {
297                                 $log = false;
298                         }
299
300                         if ((intval($this->configCache->get('system', 'db_loglimit_index_high')) > 0) && ($row['rows'] >= intval($this->configCache->get('system', 'db_loglimit_index_high')))) {
301                                 $log = true;
302                         }
303
304                         if (in_array($row['key'], $blacklist) || ($row['key'] == "")) {
305                                 $log = false;
306                         }
307
308                         if ($log) {
309                                 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
310                                 @file_put_contents($this->configCache->get('system', 'db_log_index'), DateTimeFormat::utcNow() . "\t" .
311                                                                                                       $row['key'] . "\t" . $row['rows'] . "\t" . $row['Extra'] . "\t" .
312                                                                                                       basename($backtrace[1]["file"]) . "\t" .
313                                                                                                       $backtrace[1]["line"] . "\t" . $backtrace[2]["function"] . "\t" .
314                                                                                                       substr($query, 0, 2000) . "\n", FILE_APPEND);
315                         }
316                 }
317         }
318
319         /**
320          * Removes every not whitelisted character from the identifier string
321          *
322          * @param string $identifier
323          *
324          * @return string sanitized identifier
325          * @throws \Exception
326          */
327         private function sanitizeIdentifier($identifier)
328         {
329                 return preg_replace('/[^A-Za-z0-9_\-]+/', '', $identifier);
330         }
331
332         public function escape($str)
333         {
334                 if ($this->connected) {
335                         switch ($this->driver) {
336                                 case 'pdo':
337                                         return substr(@$this->connection->quote($str, PDO::PARAM_STR), 1, -1);
338
339                                 case 'mysqli':
340                                         return @$this->connection->real_escape_string($str);
341                         }
342                 } else {
343                         return str_replace("'", "\\'", $str);
344                 }
345         }
346
347         public function isConnected()
348         {
349                 return $this->connected;
350         }
351
352         public function connected()
353         {
354                 $connected = false;
355
356                 if (is_null($this->connection)) {
357                         return false;
358                 }
359
360                 switch ($this->driver) {
361                         case 'pdo':
362                                 $r = $this->p("SELECT 1");
363                                 if ($this->isResult($r)) {
364                                         $row       = $this->toArray($r);
365                                         $connected = ($row[0]['1'] == '1');
366                                 }
367                                 break;
368                         case 'mysqli':
369                                 $connected = $this->connection->ping();
370                                 break;
371                 }
372                 return $connected;
373         }
374
375         /**
376          * @brief Replaces ANY_VALUE() function by MIN() function,
377          *  if the database server does not support ANY_VALUE().
378          *
379          * Considerations for Standard SQL, or MySQL with ONLY_FULL_GROUP_BY (default since 5.7.5).
380          * ANY_VALUE() is available from MySQL 5.7.5 https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html
381          * A standard fall-back is to use MIN().
382          *
383          * @param string $sql An SQL string without the values
384          *
385          * @return string The input SQL string modified if necessary.
386          */
387         public function anyValueFallback($sql)
388         {
389                 $server_info = $this->serverInfo();
390                 if (version_compare($server_info, '5.7.5', '<') ||
391                     (stripos($server_info, 'MariaDB') !== false)) {
392                         $sql = str_ireplace('ANY_VALUE(', 'MIN(', $sql);
393                 }
394                 return $sql;
395         }
396
397         /**
398          * @brief Replaces the ? placeholders with the parameters in the $args array
399          *
400          * @param string $sql  SQL query
401          * @param array  $args The parameters that are to replace the ? placeholders
402          *
403          * @return string The replaced SQL query
404          */
405         private function replaceParameters($sql, $args)
406         {
407                 $offset = 0;
408                 foreach ($args AS $param => $value) {
409                         if (is_int($args[$param]) || is_float($args[$param])) {
410                                 $replace = intval($args[$param]);
411                         } else {
412                                 $replace = "'" . $this->escape($args[$param]) . "'";
413                         }
414
415                         $pos = strpos($sql, '?', $offset);
416                         if ($pos !== false) {
417                                 $sql = substr_replace($sql, $replace, $pos, 1);
418                         }
419                         $offset = $pos + strlen($replace);
420                 }
421                 return $sql;
422         }
423
424         /**
425          * @brief Executes a prepared statement that returns data
426          * @usage Example: $r = p("SELECT * FROM `item` WHERE `guid` = ?", $guid);
427          *
428          * Please only use it with complicated queries.
429          * For all regular queries please use DBA::select or DBA::exists
430          *
431          * @param string $sql SQL statement
432          *
433          * @return bool|object statement object or result object
434          * @throws \Exception
435          */
436         public function p($sql)
437         {
438
439                 $stamp1 = microtime(true);
440
441                 $params = DBA::getParam(func_get_args());
442
443                 // Renumber the array keys to be sure that they fit
444                 $i    = 0;
445                 $args = [];
446                 foreach ($params AS $param) {
447                         // Avoid problems with some MySQL servers and boolean values. See issue #3645
448                         if (is_bool($param)) {
449                                 $param = (int)$param;
450                         }
451                         $args[++$i] = $param;
452                 }
453
454                 if (!$this->connected) {
455                         return false;
456                 }
457
458                 if ((substr_count($sql, '?') != count($args)) && (count($args) > 0)) {
459                         // Question: Should we continue or stop the query here?
460                         $this->logger->warning('Query parameters mismatch.', ['query' => $sql, 'args' => $args, 'callstack' => System::callstack()]);
461                 }
462
463                 $sql = DBA::cleanQuery($sql);
464                 $sql = $this->anyValueFallback($sql);
465
466                 $orig_sql = $sql;
467
468                 if ($this->configCache->get('system', 'db_callstack') !== null) {
469                         $sql = "/*" . System::callstack() . " */ " . $sql;
470                 }
471
472                 $this->error         = '';
473                 $this->errorno       = 0;
474                 $this->affected_rows = 0;
475
476                 // We have to make some things different if this function is called from "e"
477                 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
478
479                 if (isset($trace[1])) {
480                         $called_from = $trace[1];
481                 } else {
482                         // We use just something that is defined to avoid warnings
483                         $called_from = $trace[0];
484                 }
485                 // We are having an own error logging in the function "e"
486                 $called_from_e = ($called_from['function'] == 'e');
487
488                 if (!isset($this->connection)) {
489                         throw new InternalServerErrorException('The Connection is empty, although connected is set true.');
490                 }
491
492                 switch ($this->driver) {
493                         case 'pdo':
494                                 // If there are no arguments we use "query"
495                                 if (count($args) == 0) {
496                                         if (!$retval = $this->connection->query($sql)) {
497                                                 $errorInfo     = $this->connection->errorInfo();
498                                                 $this->error   = $errorInfo[2];
499                                                 $this->errorno = $errorInfo[1];
500                                                 $retval        = false;
501                                                 break;
502                                         }
503                                         $this->affected_rows = $retval->rowCount();
504                                         break;
505                                 }
506
507                                 /** @var $stmt mysqli_stmt|PDOStatement */
508                                 if (!$stmt = $this->connection->prepare($sql)) {
509                                         $errorInfo     = $this->connection->errorInfo();
510                                         $this->error   = $errorInfo[2];
511                                         $this->errorno = $errorInfo[1];
512                                         $retval        = false;
513                                         break;
514                                 }
515
516                                 foreach ($args AS $param => $value) {
517                                         if (is_int($args[$param])) {
518                                                 $data_type = PDO::PARAM_INT;
519                                         } else {
520                                                 $data_type = PDO::PARAM_STR;
521                                         }
522                                         $stmt->bindParam($param, $args[$param], $data_type);
523                                 }
524
525                                 if (!$stmt->execute()) {
526                                         $errorInfo     = $stmt->errorInfo();
527                                         $this->error   = $errorInfo[2];
528                                         $this->errorno = $errorInfo[1];
529                                         $retval        = false;
530                                 } else {
531                                         $retval              = $stmt;
532                                         $this->affected_rows = $retval->rowCount();
533                                 }
534                                 break;
535                         case 'mysqli':
536                                 // There are SQL statements that cannot be executed with a prepared statement
537                                 $parts           = explode(' ', $orig_sql);
538                                 $command         = strtolower($parts[0]);
539                                 $can_be_prepared = in_array($command, ['select', 'update', 'insert', 'delete']);
540
541                                 // The fallback routine is called as well when there are no arguments
542                                 if (!$can_be_prepared || (count($args) == 0)) {
543                                         $retval = $this->connection->query($this->replaceParameters($sql, $args));
544                                         if ($this->connection->errno) {
545                                                 $this->error   = $this->connection->error;
546                                                 $this->errorno = $this->connection->errno;
547                                                 $retval        = false;
548                                         } else {
549                                                 if (isset($retval->num_rows)) {
550                                                         $this->affected_rows = $retval->num_rows;
551                                                 } else {
552                                                         $this->affected_rows = $this->connection->affected_rows;
553                                                 }
554                                         }
555                                         break;
556                                 }
557
558                                 $stmt = $this->connection->stmt_init();
559
560                                 if (!$stmt->prepare($sql)) {
561                                         $this->error   = $stmt->error;
562                                         $this->errorno = $stmt->errno;
563                                         $retval        = false;
564                                         break;
565                                 }
566
567                                 $param_types = '';
568                                 $values      = [];
569                                 foreach ($args AS $param => $value) {
570                                         if (is_int($args[$param])) {
571                                                 $param_types .= 'i';
572                                         } elseif (is_float($args[$param])) {
573                                                 $param_types .= 'd';
574                                         } elseif (is_string($args[$param])) {
575                                                 $param_types .= 's';
576                                         } else {
577                                                 $param_types .= 'b';
578                                         }
579                                         $values[] = &$args[$param];
580                                 }
581
582                                 if (count($values) > 0) {
583                                         array_unshift($values, $param_types);
584                                         call_user_func_array([$stmt, 'bind_param'], $values);
585                                 }
586
587                                 if (!$stmt->execute()) {
588                                         $this->error   = $this->connection->error;
589                                         $this->errorno = $this->connection->errno;
590                                         $retval        = false;
591                                 } else {
592                                         $stmt->store_result();
593                                         $retval              = $stmt;
594                                         $this->affected_rows = $retval->affected_rows;
595                                 }
596                                 break;
597                 }
598
599                 // We are having an own error logging in the function "e"
600                 if (($this->errorno != 0) && !$called_from_e) {
601                         // We have to preserve the error code, somewhere in the logging it get lost
602                         $error   = $this->error;
603                         $errorno = $this->errorno;
604
605                         $this->logger->error('DB Error', [
606                                 'code'      => $this->errorno,
607                                 'error'     => $this->error,
608                                 'callstack' => System::callstack(8),
609                                 'params'    => $this->replaceParameters($sql, $args),
610                         ]);
611
612                         // On a lost connection we try to reconnect - but only once.
613                         if ($errorno == 2006) {
614                                 if ($this->in_retrial || !$this->reconnect()) {
615                                         // It doesn't make sense to continue when the database connection was lost
616                                         if ($this->in_retrial) {
617                                                 $this->logger->notice('Giving up retrial because of database error', [
618                                                         'code'  => $this->errorno,
619                                                         'error' => $this->error,
620                                                 ]);
621                                         } else {
622                                                 $this->logger->notice('Couldn\'t reconnect after database error', [
623                                                         'code'  => $this->errorno,
624                                                         'error' => $this->error,
625                                                 ]);
626                                         }
627                                         exit(1);
628                                 } else {
629                                         // We try it again
630                                         $this->logger->notice('Reconnected after database error', [
631                                                 'code'  => $this->errorno,
632                                                 'error' => $this->error,
633                                         ]);
634                                         $this->in_retrial = true;
635                                         $ret              = $this->p($sql, $args);
636                                         $this->in_retrial = false;
637                                         return $ret;
638                                 }
639                         }
640
641                         $this->error   = $error;
642                         $this->errorno = $errorno;
643                 }
644
645                 $this->profiler->saveTimestamp($stamp1, 'database', System::callstack());
646
647                 if ($this->configCache->get('system', 'db_log')) {
648                         $stamp2   = microtime(true);
649                         $duration = (float)($stamp2 - $stamp1);
650
651                         if (($duration > $this->configCache->get('system', 'db_loglimit'))) {
652                                 $duration  = round($duration, 3);
653                                 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
654
655                                 @file_put_contents($this->configCache->get('system', 'db_log'), DateTimeFormat::utcNow() . "\t" . $duration . "\t" .
656                                                                                                 basename($backtrace[1]["file"]) . "\t" .
657                                                                                                 $backtrace[1]["line"] . "\t" . $backtrace[2]["function"] . "\t" .
658                                                                                                 substr($this->replaceParameters($sql, $args), 0, 2000) . "\n", FILE_APPEND);
659                         }
660                 }
661                 return $retval;
662         }
663
664         /**
665          * @brief Executes a prepared statement like UPDATE or INSERT that doesn't return data
666          *
667          * Please use DBA::delete, DBA::insert, DBA::update, ... instead
668          *
669          * @param string $sql SQL statement
670          *
671          * @return boolean Was the query successfull? False is returned only if an error occurred
672          * @throws \Exception
673          */
674         public function e($sql)
675         {
676
677                 $stamp = microtime(true);
678
679                 $params = DBA::getParam(func_get_args());
680
681                 // In a case of a deadlock we are repeating the query 20 times
682                 $timeout = 20;
683
684                 do {
685                         $stmt = $this->p($sql, $params);
686
687                         if (is_bool($stmt)) {
688                                 $retval = $stmt;
689                         } elseif (is_object($stmt)) {
690                                 $retval = true;
691                         } else {
692                                 $retval = false;
693                         }
694
695                         $this->close($stmt);
696
697                 } while (($this->errorno == 1213) && (--$timeout > 0));
698
699                 if ($this->errorno != 0) {
700                         // We have to preserve the error code, somewhere in the logging it get lost
701                         $error   = $this->error;
702                         $errorno = $this->errorno;
703
704                         $this->logger->error('DB Error', [
705                                 'code'      => $this->errorno,
706                                 'error'     => $this->error,
707                                 'callstack' => System::callstack(8),
708                                 'params'    => $this->replaceParameters($sql, $params),
709                         ]);
710
711                         // On a lost connection we simply quit.
712                         // A reconnect like in $this->p could be dangerous with modifications
713                         if ($errorno == 2006) {
714                                 $this->logger->notice('Giving up because of database error', [
715                                         'code'  => $this->errorno,
716                                         'error' => $this->error,
717                                 ]);
718                                 exit(1);
719                         }
720
721                         $this->error   = $error;
722                         $this->errorno = $errorno;
723                 }
724
725                 $this->profiler->saveTimestamp($stamp, "database_write", System::callstack());
726
727                 return $retval;
728         }
729
730         /**
731          * @brief Check if data exists
732          *
733          * @param string|array $table     Table name or array [schema => table]
734          * @param array        $condition array of fields for condition
735          *
736          * @return boolean Are there rows for that condition?
737          * @throws \Exception
738          */
739         public function exists($table, $condition)
740         {
741                 if (empty($table)) {
742                         return false;
743                 }
744
745                 $fields = [];
746
747                 if (empty($condition)) {
748                         return DBStructure::existsTable($table);
749                 }
750
751                 reset($condition);
752                 $first_key = key($condition);
753                 if (!is_int($first_key)) {
754                         $fields = [$first_key];
755                 }
756
757                 $stmt = $this->select($table, $fields, $condition, ['limit' => 1]);
758
759                 if (is_bool($stmt)) {
760                         $retval = $stmt;
761                 } else {
762                         $retval = ($this->numRows($stmt) > 0);
763                 }
764
765                 $this->close($stmt);
766
767                 return $retval;
768         }
769
770         /**
771          * Fetches the first row
772          *
773          * Please use DBA::selectFirst or DBA::exists whenever this is possible.
774          *
775          * @brief Fetches the first row
776          *
777          * @param string $sql SQL statement
778          *
779          * @return array first row of query
780          * @throws \Exception
781          */
782         public function fetchFirst($sql)
783         {
784                 $params = DBA::getParam(func_get_args());
785
786                 $stmt = $this->p($sql, $params);
787
788                 if (is_bool($stmt)) {
789                         $retval = $stmt;
790                 } else {
791                         $retval = $this->fetch($stmt);
792                 }
793
794                 $this->close($stmt);
795
796                 return $retval;
797         }
798
799         /**
800          * @brief Returns the number of affected rows of the last statement
801          *
802          * @return int Number of rows
803          */
804         public function affectedRows()
805         {
806                 return $this->affected_rows;
807         }
808
809         /**
810          * @brief Returns the number of columns of a statement
811          *
812          * @param object Statement object
813          *
814          * @return int Number of columns
815          */
816         public function columnCount($stmt)
817         {
818                 if (!is_object($stmt)) {
819                         return 0;
820                 }
821                 switch ($this->driver) {
822                         case 'pdo':
823                                 return $stmt->columnCount();
824                         case 'mysqli':
825                                 return $stmt->field_count;
826                 }
827                 return 0;
828         }
829
830         /**
831          * @brief Returns the number of rows of a statement
832          *
833          * @param PDOStatement|mysqli_result|mysqli_stmt Statement object
834          *
835          * @return int Number of rows
836          */
837         public function numRows($stmt)
838         {
839                 if (!is_object($stmt)) {
840                         return 0;
841                 }
842                 switch ($this->driver) {
843                         case 'pdo':
844                                 return $stmt->rowCount();
845                         case 'mysqli':
846                                 return $stmt->num_rows;
847                 }
848                 return 0;
849         }
850
851         /**
852          * @brief Fetch a single row
853          *
854          * @param mixed $stmt statement object
855          *
856          * @return array current row
857          */
858         public function fetch($stmt)
859         {
860
861                 $stamp1 = microtime(true);
862
863                 $columns = [];
864
865                 if (!is_object($stmt)) {
866                         return false;
867                 }
868
869                 switch ($this->driver) {
870                         case 'pdo':
871                                 $columns = $stmt->fetch(PDO::FETCH_ASSOC);
872                                 break;
873                         case 'mysqli':
874                                 if (get_class($stmt) == 'mysqli_result') {
875                                         $columns = $stmt->fetch_assoc();
876                                         break;
877                                 }
878
879                                 // This code works, but is slow
880
881                                 // Bind the result to a result array
882                                 $cols = [];
883
884                                 $cols_num = [];
885                                 for ($x = 0; $x < $stmt->field_count; $x++) {
886                                         $cols[] = &$cols_num[$x];
887                                 }
888
889                                 call_user_func_array([$stmt, 'bind_result'], $cols);
890
891                                 if (!$stmt->fetch()) {
892                                         return false;
893                                 }
894
895                                 // The slow part:
896                                 // We need to get the field names for the array keys
897                                 // It seems that there is no better way to do this.
898                                 $result = $stmt->result_metadata();
899                                 $fields = $result->fetch_fields();
900
901                                 foreach ($cols_num AS $param => $col) {
902                                         $columns[$fields[$param]->name] = $col;
903                                 }
904                 }
905
906                 $this->profiler->saveTimestamp($stamp1, 'database', System::callstack());
907
908                 return $columns;
909         }
910
911         /**
912          * @brief Insert a row into a table
913          *
914          * @param string|array $table               Table name or array [schema => table]
915          * @param array        $param               parameter array
916          * @param bool         $on_duplicate_update Do an update on a duplicate entry
917          *
918          * @return boolean was the insert successful?
919          * @throws \Exception
920          */
921         public function insert($table, $param, $on_duplicate_update = false)
922         {
923                 if (empty($table) || empty($param)) {
924                         $this->logger->info('Table and fields have to be set');
925                         return false;
926                 }
927
928                 $table_string = DBA::buildTableString($table);
929
930                 $fields_string = implode(', ', array_map([DBA::class, 'quoteIdentifier'], array_keys($param)));
931
932                 $values_string = substr(str_repeat("?, ", count($param)), 0, -2);
933
934                 $sql = "INSERT INTO " . $table_string . " (" . $fields_string . ") VALUES (" . $values_string . ")";
935
936                 if ($on_duplicate_update) {
937                         $fields_string = implode(' = ?, ', array_map([DBA::class, 'quoteIdentifier'], array_keys($param)));
938
939                         $sql .= " ON DUPLICATE KEY UPDATE " . $fields_string . " = ?";
940
941                         $values = array_values($param);
942                         $param  = array_merge_recursive($values, $values);
943                 }
944
945                 return $this->e($sql, $param);
946         }
947
948         /**
949          * @brief Fetch the id of the last insert command
950          *
951          * @return integer Last inserted id
952          */
953         public function lastInsertId()
954         {
955                 switch ($this->driver) {
956                         case 'pdo':
957                                 $id = $this->connection->lastInsertId();
958                                 break;
959                         case 'mysqli':
960                                 $id = $this->connection->insert_id;
961                                 break;
962                 }
963                 return $id;
964         }
965
966         /**
967          * @brief Locks a table for exclusive write access
968          *
969          * This function can be extended in the future to accept a table array as well.
970          *
971          * @param string|array $table Table name or array [schema => table]
972          *
973          * @return boolean was the lock successful?
974          * @throws \Exception
975          */
976         public function lock($table)
977         {
978                 // See here: https://dev.mysql.com/doc/refman/5.7/en/lock-tables-and-transactions.html
979                 if ($this->driver == 'pdo') {
980                         $this->e("SET autocommit=0");
981                         $this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
982                 } else {
983                         $this->connection->autocommit(false);
984                 }
985
986                 $success = $this->e("LOCK TABLES " . DBA::buildTableString($table) . " WRITE");
987
988                 if ($this->driver == 'pdo') {
989                         $this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
990                 }
991
992                 if (!$success) {
993                         if ($this->driver == 'pdo') {
994                                 $this->e("SET autocommit=1");
995                         } else {
996                                 $this->connection->autocommit(true);
997                         }
998                 } else {
999                         $this->in_transaction = true;
1000                 }
1001                 return $success;
1002         }
1003
1004         /**
1005          * @brief Unlocks all locked tables
1006          *
1007          * @return boolean was the unlock successful?
1008          * @throws \Exception
1009          */
1010         public function unlock()
1011         {
1012                 // See here: https://dev.mysql.com/doc/refman/5.7/en/lock-tables-and-transactions.html
1013                 $this->performCommit();
1014
1015                 if ($this->driver == 'pdo') {
1016                         $this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
1017                 }
1018
1019                 $success = $this->e("UNLOCK TABLES");
1020
1021                 if ($this->driver == 'pdo') {
1022                         $this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
1023                         $this->e("SET autocommit=1");
1024                 } else {
1025                         $this->connection->autocommit(true);
1026                 }
1027
1028                 $this->in_transaction = false;
1029                 return $success;
1030         }
1031
1032         /**
1033          * @brief Starts a transaction
1034          *
1035          * @return boolean Was the command executed successfully?
1036          */
1037         public function transaction()
1038         {
1039                 if (!$this->performCommit()) {
1040                         return false;
1041                 }
1042
1043                 switch ($this->driver) {
1044                         case 'pdo':
1045                                 if (!$this->connection->inTransaction() && !$this->connection->beginTransaction()) {
1046                                         return false;
1047                                 }
1048                                 break;
1049
1050                         case 'mysqli':
1051                                 if (!$this->connection->begin_transaction()) {
1052                                         return false;
1053                                 }
1054                                 break;
1055                 }
1056
1057                 $this->in_transaction = true;
1058                 return true;
1059         }
1060
1061         protected function performCommit()
1062         {
1063                 switch ($this->driver) {
1064                         case 'pdo':
1065                                 if (!$this->connection->inTransaction()) {
1066                                         return true;
1067                                 }
1068
1069                                 return $this->connection->commit();
1070
1071                         case 'mysqli':
1072                                 return $this->connection->commit();
1073                 }
1074
1075                 return true;
1076         }
1077
1078         /**
1079          * @brief Does a commit
1080          *
1081          * @return boolean Was the command executed successfully?
1082          */
1083         public function commit()
1084         {
1085                 if (!$this->performCommit()) {
1086                         return false;
1087                 }
1088                 $this->in_transaction = false;
1089                 return true;
1090         }
1091
1092         /**
1093          * @brief Does a rollback
1094          *
1095          * @return boolean Was the command executed successfully?
1096          */
1097         public function rollback()
1098         {
1099                 $ret = false;
1100
1101                 switch ($this->driver) {
1102                         case 'pdo':
1103                                 if (!$this->connection->inTransaction()) {
1104                                         $ret = true;
1105                                         break;
1106                                 }
1107                                 $ret = $this->connection->rollBack();
1108                                 break;
1109
1110                         case 'mysqli':
1111                                 $ret = $this->connection->rollback();
1112                                 break;
1113                 }
1114                 $this->in_transaction = false;
1115                 return $ret;
1116         }
1117
1118         /**
1119          * @brief Build the array with the table relations
1120          *
1121          * The array is build from the database definitions in DBStructure.php
1122          *
1123          * This process must only be started once, since the value is cached.
1124          */
1125         private function buildRelationData()
1126         {
1127                 $definition = DBStructure::definition($this->configCache->get('system', 'basepath'));
1128
1129                 foreach ($definition AS $table => $structure) {
1130                         foreach ($structure['fields'] AS $field => $field_struct) {
1131                                 if (isset($field_struct['relation'])) {
1132                                         foreach ($field_struct['relation'] AS $rel_table => $rel_field) {
1133                                                 $this->relation[$rel_table][$rel_field][$table][] = $field;
1134                                         }
1135                                 }
1136                         }
1137                 }
1138         }
1139
1140         /**
1141          * @brief Delete a row from a table
1142          *
1143          * Note: this methods does NOT accept schema => table arrays because of the complex relation stuff.
1144          *
1145          * @param string $table      Table name
1146          * @param array  $conditions Field condition(s)
1147          * @param array  $options
1148          *                           - cascade: If true we delete records in other tables that depend on the one we're deleting through
1149          *                           relations (default: true)
1150          * @param array  $callstack  Internal use: prevent endless loops
1151          *
1152          * @return boolean was the delete successful?
1153          * @throws \Exception
1154          */
1155         public function delete($table, array $conditions, array $options = [], array &$callstack = [])
1156         {
1157                 if (empty($table) || empty($conditions)) {
1158                         $this->logger->info('Table and conditions have to be set');
1159                         return false;
1160                 }
1161
1162                 $commands = [];
1163
1164                 // Create a key for the loop prevention
1165                 $key = $table . ':' . json_encode($conditions);
1166
1167                 // We quit when this key already exists in the callstack.
1168                 if (isset($callstack[$key])) {
1169                         return true;
1170                 }
1171
1172                 $callstack[$key] = true;
1173
1174                 $commands[$key] = ['table' => $table, 'conditions' => $conditions];
1175
1176                 // Don't use "defaults" here, since it would set "false" to "true"
1177                 if (isset($options['cascade'])) {
1178                         $cascade = $options['cascade'];
1179                 } else {
1180                         $cascade = true;
1181                 }
1182
1183                 // To speed up the whole process we cache the table relations
1184                 if ($cascade && count($this->relation) == 0) {
1185                         $this->buildRelationData();
1186                 }
1187
1188                 // Is there a relation entry for the table?
1189                 if ($cascade && isset($this->relation[$table])) {
1190                         // We only allow a simple "one field" relation.
1191                         $field   = array_keys($this->relation[$table])[0];
1192                         $rel_def = array_values($this->relation[$table])[0];
1193
1194                         // Create a key for preventing double queries
1195                         $qkey = $field . '-' . $table . ':' . json_encode($conditions);
1196
1197                         // When the search field is the relation field, we don't need to fetch the rows
1198                         // This is useful when the leading record is already deleted in the frontend but the rest is done in the backend
1199                         if ((count($conditions) == 1) && ($field == array_keys($conditions)[0])) {
1200                                 foreach ($rel_def AS $rel_table => $rel_fields) {
1201                                         foreach ($rel_fields AS $rel_field) {
1202                                                 $this->delete($rel_table, [$rel_field => array_values($conditions)[0]], $options, $callstack);
1203                                         }
1204                                 }
1205                                 // We quit when this key already exists in the callstack.
1206                         } elseif (!isset($callstack[$qkey])) {
1207                                 $callstack[$qkey] = true;
1208
1209                                 // Fetch all rows that are to be deleted
1210                                 $data = $this->select($table, [$field], $conditions);
1211
1212                                 while ($row = $this->fetch($data)) {
1213                                         $this->delete($table, [$field => $row[$field]], $options, $callstack);
1214                                 }
1215
1216                                 $this->close($data);
1217
1218                                 // Since we had split the delete command we don't need the original command anymore
1219                                 unset($commands[$key]);
1220                         }
1221                 }
1222
1223                 // Now we finalize the process
1224                 $do_transaction = !$this->in_transaction;
1225
1226                 if ($do_transaction) {
1227                         $this->transaction();
1228                 }
1229
1230                 $compacted = [];
1231                 $counter   = [];
1232
1233                 foreach ($commands AS $command) {
1234                         $conditions = $command['conditions'];
1235                         reset($conditions);
1236                         $first_key = key($conditions);
1237
1238                         $condition_string = DBA::buildCondition($conditions);
1239
1240                         if ((count($command['conditions']) > 1) || is_int($first_key)) {
1241                                 $sql = "DELETE FROM " . DBA::quoteIdentifier($command['table']) . " " . $condition_string;
1242                                 $this->logger->debug($this->replaceParameters($sql, $conditions));
1243
1244                                 if (!$this->e($sql, $conditions)) {
1245                                         if ($do_transaction) {
1246                                                 $this->rollback();
1247                                         }
1248                                         return false;
1249                                 }
1250                         } else {
1251                                 $key_table     = $command['table'];
1252                                 $key_condition = array_keys($command['conditions'])[0];
1253                                 $value         = array_values($command['conditions'])[0];
1254
1255                                 // Split the SQL queries in chunks of 100 values
1256                                 // We do the $i stuff here to make the code better readable
1257                                 $i = isset($counter[$key_table][$key_condition]) ? $counter[$key_table][$key_condition] : 0;
1258                                 if (isset($compacted[$key_table][$key_condition][$i]) && count($compacted[$key_table][$key_condition][$i]) > 100) {
1259                                         ++$i;
1260                                 }
1261
1262                                 $compacted[$key_table][$key_condition][$i][$value] = $value;
1263                                 $counter[$key_table][$key_condition]               = $i;
1264                         }
1265                 }
1266                 foreach ($compacted AS $table => $values) {
1267                         foreach ($values AS $field => $field_value_list) {
1268                                 foreach ($field_value_list AS $field_values) {
1269                                         $sql = "DELETE FROM " . DBA::quoteIdentifier($table) . " WHERE " . DBA::quoteIdentifier($field) . " IN (" .
1270                                                substr(str_repeat("?, ", count($field_values)), 0, -2) . ");";
1271
1272                                         $this->logger->debug($this->replaceParameters($sql, $field_values));
1273
1274                                         if (!$this->e($sql, $field_values)) {
1275                                                 if ($do_transaction) {
1276                                                         $this->rollback();
1277                                                 }
1278                                                 return false;
1279                                         }
1280                                 }
1281                         }
1282                 }
1283                 if ($do_transaction) {
1284                         $this->commit();
1285                 }
1286                 return true;
1287         }
1288
1289         /**
1290          * @brief Updates rows
1291          *
1292          * Updates rows in the database. When $old_fields is set to an array,
1293          * the system will only do an update if the fields in that array changed.
1294          *
1295          * Attention:
1296          * Only the values in $old_fields are compared.
1297          * This is an intentional behaviour.
1298          *
1299          * Example:
1300          * We include the timestamp field in $fields but not in $old_fields.
1301          * Then the row will only get the new timestamp when the other fields had changed.
1302          *
1303          * When $old_fields is set to a boolean value the system will do this compare itself.
1304          * When $old_fields is set to "true" the system will do an insert if the row doesn't exists.
1305          *
1306          * Attention:
1307          * Only set $old_fields to a boolean value when you are sure that you will update a single row.
1308          * When you set $old_fields to "true" then $fields must contain all relevant fields!
1309          *
1310          * @param string|array  $table      Table name or array [schema => table]
1311          * @param array         $fields     contains the fields that are updated
1312          * @param array         $condition  condition array with the key values
1313          * @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate)
1314          *
1315          * @return boolean was the update successfull?
1316          * @throws \Exception
1317          */
1318         public function update($table, $fields, $condition, $old_fields = [])
1319         {
1320                 if (empty($table) || empty($fields) || empty($condition)) {
1321                         $this->logger->info('Table, fields and condition have to be set');
1322                         return false;
1323                 }
1324
1325                 $table_string = DBA::buildTableString($table);
1326
1327                 $condition_string = DBA::buildCondition($condition);
1328
1329                 if (is_bool($old_fields)) {
1330                         $do_insert = $old_fields;
1331
1332                         $old_fields = $this->selectFirst($table, [], $condition);
1333
1334                         if (is_bool($old_fields)) {
1335                                 if ($do_insert) {
1336                                         $values = array_merge($condition, $fields);
1337                                         return $this->insert($table, $values, $do_insert);
1338                                 }
1339                                 $old_fields = [];
1340                         }
1341                 }
1342
1343                 $do_update = (count($old_fields) == 0);
1344
1345                 foreach ($old_fields AS $fieldname => $content) {
1346                         if (isset($fields[$fieldname])) {
1347                                 if (($fields[$fieldname] == $content) && !is_null($content)) {
1348                                         unset($fields[$fieldname]);
1349                                 } else {
1350                                         $do_update = true;
1351                                 }
1352                         }
1353                 }
1354
1355                 if (!$do_update || (count($fields) == 0)) {
1356                         return true;
1357                 }
1358
1359                 $sql = "UPDATE " . $table_string . " SET "
1360                         . implode(" = ?, ", array_map([DBA::class, 'quoteIdentifier'], array_keys($fields))) . " = ?"
1361                         . $condition_string;
1362
1363                 $params1 = array_values($fields);
1364                 $params2 = array_values($condition);
1365                 $params  = array_merge_recursive($params1, $params2);
1366
1367                 return $this->e($sql, $params);
1368         }
1369
1370         /**
1371          * Retrieve a single record from a table and returns it in an associative array
1372          *
1373          * @brief Retrieve a single record from a table
1374          *
1375          * @param string $table
1376          * @param array  $fields
1377          * @param array  $condition
1378          * @param array  $params
1379          *
1380          * @return bool|array
1381          * @throws \Exception
1382          * @see   $this->select
1383          */
1384         public function selectFirst($table, array $fields = [], array $condition = [], $params = [])
1385         {
1386                 $params['limit'] = 1;
1387                 $result          = $this->select($table, $fields, $condition, $params);
1388
1389                 if (is_bool($result)) {
1390                         return $result;
1391                 } else {
1392                         $row = $this->fetch($result);
1393                         $this->close($result);
1394                         return $row;
1395                 }
1396         }
1397
1398         /**
1399          * @brief Select rows from a table and fills an array with the data
1400          *
1401          * @param string|array $table     Table name or array [schema => table]
1402          * @param array        $fields    Array of selected fields, empty for all
1403          * @param array        $condition Array of fields for condition
1404          * @param array        $params    Array of several parameters
1405          *
1406          * @return array Data array
1407          * @throws \Exception
1408          * @see   self::select
1409          */
1410         public function selectToArray(string $table, array $fields = [], array $condition = [], array $params = [])
1411         {
1412                 return $this->toArray($this->select($table, $fields, $condition, $params));
1413         }
1414
1415         /**
1416          * @brief Select rows from a table
1417          *
1418          * @param string|array $table     Table name or array [schema => table]
1419          * @param array        $fields    Array of selected fields, empty for all
1420          * @param array        $condition Array of fields for condition
1421          * @param array        $params    Array of several parameters
1422          *
1423          * @return boolean|object
1424          *
1425          * Example:
1426          * $table = "item";
1427          * $fields = array("id", "uri", "uid", "network");
1428          *
1429          * $condition = array("uid" => 1, "network" => 'dspr');
1430          * or:
1431          * $condition = array("`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr');
1432          *
1433          * $params = array("order" => array("id", "received" => true), "limit" => 10);
1434          *
1435          * $data = DBA::select($table, $fields, $condition, $params);
1436          * @throws \Exception
1437          */
1438         public function select($table, array $fields = [], array $condition = [], array $params = [])
1439         {
1440                 if (empty($table)) {
1441                         return false;
1442                 }
1443
1444                 if (count($fields) > 0) {
1445                         $select_string = implode(', ', array_map([DBA::class, 'quoteIdentifier'], $fields));
1446                 } else {
1447                         $select_string = '*';
1448                 }
1449
1450                 $table_string = DBA::buildTableString($table);
1451
1452                 $condition_string = DBA::buildCondition($condition);
1453
1454                 $param_string = DBA::buildParameter($params);
1455
1456                 $sql = "SELECT " . $select_string . " FROM " . $table_string . $condition_string . $param_string;
1457
1458                 $result = $this->p($sql, $condition);
1459
1460                 return $result;
1461         }
1462
1463         /**
1464          * @brief Counts the rows from a table satisfying the provided condition
1465          *
1466          * @param string|array $table     Table name or array [schema => table]
1467          * @param array        $condition Array of fields for condition
1468          *
1469          * @return int
1470          *
1471          * Example:
1472          * $table = "item";
1473          *
1474          * $condition = ["uid" => 1, "network" => 'dspr'];
1475          * or:
1476          * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
1477          *
1478          * $count = DBA::count($table, $condition);
1479          * @throws \Exception
1480          */
1481         public function count($table, array $condition = [])
1482         {
1483                 if (empty($table)) {
1484                         return false;
1485                 }
1486
1487                 $table_string = DBA::buildTableString($table);
1488
1489                 $condition_string = DBA::buildCondition($condition);
1490
1491                 $sql = "SELECT COUNT(*) AS `count` FROM " . $table_string . $condition_string;
1492
1493                 $row = $this->fetchFirst($sql, $condition);
1494
1495                 return $row['count'];
1496         }
1497
1498         /**
1499          * @brief Fills an array with data from a query
1500          *
1501          * @param object $stmt statement object
1502          * @param bool   $do_close
1503          *
1504          * @return array Data array
1505          */
1506         public function toArray($stmt, $do_close = true)
1507         {
1508                 if (is_bool($stmt)) {
1509                         return [];
1510                 }
1511
1512                 $data = [];
1513                 while ($row = $this->fetch($stmt)) {
1514                         $data[] = $row;
1515                 }
1516
1517                 if ($do_close) {
1518                         $this->close($stmt);
1519                 }
1520
1521                 return $data;
1522         }
1523
1524         /**
1525          * @brief Returns the error number of the last query
1526          *
1527          * @return string Error number (0 if no error)
1528          */
1529         public function errorNo()
1530         {
1531                 return $this->errorno;
1532         }
1533
1534         /**
1535          * @brief Returns the error message of the last query
1536          *
1537          * @return string Error message ('' if no error)
1538          */
1539         public function errorMessage()
1540         {
1541                 return $this->error;
1542         }
1543
1544         /**
1545          * @brief Closes the current statement
1546          *
1547          * @param object $stmt statement object
1548          *
1549          * @return boolean was the close successful?
1550          */
1551         public function close($stmt)
1552         {
1553
1554                 $stamp1 = microtime(true);
1555
1556                 if (!is_object($stmt)) {
1557                         return false;
1558                 }
1559
1560                 switch ($this->driver) {
1561                         case 'pdo':
1562                                 $ret = $stmt->closeCursor();
1563                                 break;
1564                         case 'mysqli':
1565                                 // MySQLi offers both a mysqli_stmt and a mysqli_result class.
1566                                 // We should be careful not to assume the object type of $stmt
1567                                 // because DBA::p() has been able to return both types.
1568                                 if ($stmt instanceof mysqli_stmt) {
1569                                         $stmt->free_result();
1570                                         $ret = $stmt->close();
1571                                 } elseif ($stmt instanceof mysqli_result) {
1572                                         $stmt->free();
1573                                         $ret = true;
1574                                 } else {
1575                                         $ret = false;
1576                                 }
1577                                 break;
1578                 }
1579
1580                 $this->profiler->saveTimestamp($stamp1, 'database', System::callstack());
1581
1582                 return $ret;
1583         }
1584
1585         /**
1586          * @brief Return a list of database processes
1587          *
1588          * @return array
1589          *      'list' => List of processes, separated in their different states
1590          *      'amount' => Number of concurrent database processes
1591          * @throws \Exception
1592          */
1593         public function processlist()
1594         {
1595                 $ret  = $this->p("SHOW PROCESSLIST");
1596                 $data = $this->toArray($ret);
1597
1598                 $processes = 0;
1599                 $states    = [];
1600                 foreach ($data as $process) {
1601                         $state = trim($process["State"]);
1602
1603                         // Filter out all non blocking processes
1604                         if (!in_array($state, ["", "init", "statistics", "updating"])) {
1605                                 ++$states[$state];
1606                                 ++$processes;
1607                         }
1608                 }
1609
1610                 $statelist = "";
1611                 foreach ($states as $state => $usage) {
1612                         if ($statelist != "") {
1613                                 $statelist .= ", ";
1614                         }
1615                         $statelist .= $state . ": " . $usage;
1616                 }
1617                 return (["list" => $statelist, "amount" => $processes]);
1618         }
1619
1620         /**
1621          * Checks if $array is a filled array with at least one entry.
1622          *
1623          * @param mixed $array A filled array with at least one entry
1624          *
1625          * @return boolean Whether $array is a filled array or an object with rows
1626          */
1627         public function isResult($array)
1628         {
1629                 // It could be a return value from an update statement
1630                 if (is_bool($array)) {
1631                         return $array;
1632                 }
1633
1634                 if (is_object($array)) {
1635                         return $this->numRows($array) > 0;
1636                 }
1637
1638                 return (is_array($array) && (count($array) > 0));
1639         }
1640
1641         /**
1642          * @brief Callback function for "esc_array"
1643          *
1644          * @param mixed   $value         Array value
1645          * @param string  $key           Array key
1646          * @param boolean $add_quotation add quotation marks for string values
1647          *
1648          * @return void
1649          */
1650         private function escapeArrayCallback(&$value, $key, $add_quotation)
1651         {
1652                 if (!$add_quotation) {
1653                         if (is_bool($value)) {
1654                                 $value = ($value ? '1' : '0');
1655                         } else {
1656                                 $value = $this->escape($value);
1657                         }
1658                         return;
1659                 }
1660
1661                 if (is_bool($value)) {
1662                         $value = ($value ? 'true' : 'false');
1663                 } elseif (is_float($value) || is_integer($value)) {
1664                         $value = (string)$value;
1665                 } else {
1666                         $value = "'" . $this->escape($value) . "'";
1667                 }
1668         }
1669
1670         /**
1671          * @brief Escapes a whole array
1672          *
1673          * @param mixed   $arr           Array with values to be escaped
1674          * @param boolean $add_quotation add quotation marks for string values
1675          *
1676          * @return void
1677          */
1678         public function escapeArray(&$arr, $add_quotation = false)
1679         {
1680                 array_walk($arr, [$this, 'escapeArrayCallback'], $add_quotation);
1681         }
1682 }