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