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