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