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