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