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