]> git.mxchange.org Git - friendica.git/blob - include/dba.php
New function "dba::select" to fetch table rows
[friendica.git] / include / dba.php
1 <?php
2 require_once("dbm.php");
3 require_once('include/datetime.php');
4
5 /**
6  * @class MySQL database class
7  *
8  * For debugging, insert 'dbg(1);' anywhere in the program flow.
9  * dbg(0); will turn it off. Logging is performed at LOGGER_DATA level.
10  * When logging, all binary info is converted to text and html entities are escaped so that
11  * the debugging stream is safe to view within both terminals and web pages.
12  *
13  * This class is for the low level database stuff that does driver specific things.
14  */
15
16 class dba {
17
18         private $debug = 0;
19         private $db;
20         private $result;
21         private $driver;
22         public  $connected = false;
23         public  $error = false;
24         private $_server_info = '';
25         private static $dbo;
26         private static $relation = array();
27
28         function __construct($serveraddr, $user, $pass, $db, $install = false) {
29                 $a = get_app();
30
31                 $stamp1 = microtime(true);
32
33                 $serveraddr = trim($serveraddr);
34
35                 $serverdata = explode(':', $serveraddr);
36                 $server = $serverdata[0];
37
38                 if (count($serverdata) > 1) {
39                         $port = trim($serverdata[1]);
40                 }
41
42                 $server = trim($server);
43                 $user = trim($user);
44                 $pass = trim($pass);
45                 $db = trim($db);
46
47                 if (!(strlen($server) && strlen($user))) {
48                         $this->connected = false;
49                         $this->db = null;
50                         return;
51                 }
52
53                 if ($install) {
54                         if (strlen($server) && ($server !== 'localhost') && ($server !== '127.0.0.1')) {
55                                 if (! dns_get_record($server, DNS_A + DNS_CNAME + DNS_PTR)) {
56                                         $this->error = sprintf(t('Cannot locate DNS info for database server \'%s\''), $server);
57                                         $this->connected = false;
58                                         $this->db = null;
59                                         return;
60                                 }
61                         }
62                 }
63
64                 if (class_exists('\PDO') && in_array('mysql', PDO::getAvailableDrivers())) {
65                         $this->driver = 'pdo';
66                         $connect = "mysql:host=".$server.";dbname=".$db;
67
68                         if (isset($port)) {
69                                 $connect .= ";port=".$port;
70                         }
71
72                         if (isset($a->config["system"]["db_charset"])) {
73                                 $connect .= ";charset=".$a->config["system"]["db_charset"];
74                         }
75                         $this->db = @new PDO($connect, $user, $pass);
76                         if (!$this->db->errorCode()) {
77                                 $this->connected = true;
78                         }
79                 } elseif (class_exists('mysqli')) {
80                         $this->driver = 'mysqli';
81                         $this->db = @new mysqli($server, $user, $pass, $db, $port);
82                         if (!mysqli_connect_errno()) {
83                                 $this->connected = true;
84
85                                 if (isset($a->config["system"]["db_charset"])) {
86                                         $this->db->set_charset($a->config["system"]["db_charset"]);
87                                 }
88                         }
89                 } elseif (function_exists('mysql_connect')) {
90                         $this->driver = 'mysql';
91                         $this->db = mysql_connect($serveraddr, $user, $pass);
92                         if ($this->db && mysql_select_db($db, $this->db)) {
93                                 $this->connected = true;
94
95                                 if (isset($a->config["system"]["db_charset"])) {
96                                         mysql_set_charset($a->config["system"]["db_charset"], $this->db);
97                                 }
98                         }
99                 } else {
100                         // No suitable SQL driver was found.
101                         if (!$install) {
102                                 system_unavailable();
103                         }
104                 }
105
106                 if (!$this->connected) {
107                         $this->db = null;
108                         if (!$install) {
109                                 system_unavailable();
110                         }
111                 }
112                 $a->save_timestamp($stamp1, "network");
113
114                 self::$dbo = $this;
115         }
116
117         /**
118          * @brief Returns the MySQL server version string
119          * 
120          * This function discriminate between the deprecated mysql API and the current
121          * object-oriented mysqli API. Example of returned string: 5.5.46-0+deb8u1
122          *
123          * @return string
124          */
125         public function server_info() {
126                 if ($this->_server_info == '') {
127                         switch ($this->driver) {
128                                 case 'pdo':
129                                         $this->_server_info = $this->db->getAttribute(PDO::ATTR_SERVER_VERSION);
130                                         break;
131                                 case 'mysqli':
132                                         $this->_server_info = $this->db->server_info;
133                                         break;
134                                 case 'mysql':
135                                         $this->_server_info = mysql_get_server_info($this->db);
136                                         break;
137                         }
138                 }
139                 return $this->_server_info;
140         }
141
142         /**
143          * @brief Returns the selected database name
144          *
145          * @return string
146          */
147         public function database_name() {
148                 $r = $this->q("SELECT DATABASE() AS `db`");
149
150                 return $r[0]['db'];
151         }
152
153         /**
154          * @brief Analyze a database query and log this if some conditions are met.
155          *
156          * @param string $query The database query that will be analyzed
157          */
158         public function log_index($query) {
159                 $a = get_app();
160
161                 if ($a->config["system"]["db_log_index"] == "") {
162                         return;
163                 }
164
165                 // Don't explain an explain statement
166                 if (strtolower(substr($query, 0, 7)) == "explain") {
167                         return;
168                 }
169
170                 // Only do the explain on "select", "update" and "delete"
171                 if (!in_array(strtolower(substr($query, 0, 6)), array("select", "update", "delete"))) {
172                         return;
173                 }
174
175                 $r = $this->q("EXPLAIN ".$query);
176                 if (!dbm::is_result($r)) {
177                         return;
178                 }
179
180                 $watchlist = explode(',', $a->config["system"]["db_log_index_watch"]);
181                 $blacklist = explode(',', $a->config["system"]["db_log_index_blacklist"]);
182
183                 foreach ($r AS $row) {
184                         if ((intval($a->config["system"]["db_loglimit_index"]) > 0)) {
185                                 $log = (in_array($row['key'], $watchlist) AND
186                                         ($row['rows'] >= intval($a->config["system"]["db_loglimit_index"])));
187                         } else {
188                                 $log = false;
189                         }
190
191                         if ((intval($a->config["system"]["db_loglimit_index_high"]) > 0) AND ($row['rows'] >= intval($a->config["system"]["db_loglimit_index_high"]))) {
192                                 $log = true;
193                         }
194
195                         if (in_array($row['key'], $blacklist) OR ($row['key'] == "")) {
196                                 $log = false;
197                         }
198
199                         if ($log) {
200                                 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
201                                 @file_put_contents($a->config["system"]["db_log_index"], datetime_convert()."\t".
202                                                 $row['key']."\t".$row['rows']."\t".$row['Extra']."\t".
203                                                 basename($backtrace[1]["file"])."\t".
204                                                 $backtrace[1]["line"]."\t".$backtrace[2]["function"]."\t".
205                                                 substr($query, 0, 2000)."\n", FILE_APPEND);
206                         }
207                 }
208         }
209
210         public function q($sql, $onlyquery = false) {
211                 $a = get_app();
212
213                 if (!$this->db || !$this->connected) {
214                         return false;
215                 }
216
217                 $this->error = '';
218
219                 $connstr = ($this->connected() ? "Connected" : "Disonnected");
220
221                 $stamp1 = microtime(true);
222
223                 $orig_sql = $sql;
224
225                 if (x($a->config,'system') && x($a->config['system'], 'db_callstack')) {
226                         $sql = "/*".$a->callstack()." */ ".$sql;
227                 }
228
229                 $columns = 0;
230
231                 switch ($this->driver) {
232                         case 'pdo':
233                                 $result = @$this->db->query($sql);
234                                 // Is used to separate between queries that returning data - or not
235                                 if (!is_bool($result)) {
236                                         $columns = $result->columnCount();
237                                 }
238                                 break;
239                         case 'mysqli':
240                                 $result = @$this->db->query($sql);
241                                 break;
242                         case 'mysql':
243                                 $result = @mysql_query($sql,$this->db);
244                                 break;
245                 }
246                 $stamp2 = microtime(true);
247                 $duration = (float)($stamp2 - $stamp1);
248
249                 $a->save_timestamp($stamp1, "database");
250
251                 if (strtolower(substr($orig_sql, 0, 6)) != "select") {
252                         $a->save_timestamp($stamp1, "database_write");
253                 }
254                 if (x($a->config,'system') && x($a->config['system'],'db_log')) {
255                         if (($duration > $a->config["system"]["db_loglimit"])) {
256                                 $duration = round($duration, 3);
257                                 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
258                                 @file_put_contents($a->config["system"]["db_log"], datetime_convert()."\t".$duration."\t".
259                                                 basename($backtrace[1]["file"])."\t".
260                                                 $backtrace[1]["line"]."\t".$backtrace[2]["function"]."\t".
261                                                 substr($sql, 0, 2000)."\n", FILE_APPEND);
262                         }
263                 }
264
265                 switch ($this->driver) {
266                         case 'pdo':
267                                 $errorInfo = $this->db->errorInfo();
268                                 if ($errorInfo) {
269                                         $this->error = $errorInfo[2];
270                                         $this->errorno = $errorInfo[1];
271                                 }
272                                 break;
273                         case 'mysqli':
274                                 if ($this->db->errno) {
275                                         $this->error = $this->db->error;
276                                         $this->errorno = $this->db->errno;
277                                 }
278                                 break;
279                         case 'mysql':
280                                 if (mysql_errno($this->db)) {
281                                         $this->error = mysql_error($this->db);
282                                         $this->errorno = mysql_errno($this->db);
283                                 }
284                                 break;
285                 }
286                 if (strlen($this->error)) {
287                         logger('DB Error ('.$connstr.') '.$this->errorno.': '.$this->error);
288                 }
289
290                 if ($this->debug) {
291
292                         $mesg = '';
293
294                         if ($result === false) {
295                                 $mesg = 'false';
296                         } elseif ($result === true) {
297                                 $mesg = 'true';
298                         } else {
299                                 switch ($this->driver) {
300                                         case 'pdo':
301                                                 $mesg = $result->rowCount().' results'.EOL;
302                                                 break;
303                                         case 'mysqli':
304                                                 $mesg = $result->num_rows.' results'.EOL;
305                                                 break;
306                                         case 'mysql':
307                                                 $mesg = mysql_num_rows($result).' results'.EOL;
308                                                 break;
309                                 }
310                         }
311
312                         $str =  'SQL = ' . printable($sql) . EOL . 'SQL returned ' . $mesg
313                                 . (($this->error) ? ' error: ' . $this->error : '')
314                                 . EOL;
315
316                         logger('dba: ' . $str );
317                 }
318
319                 /**
320                  * If dbfail.out exists, we will write any failed calls directly to it,
321                  * regardless of any logging that may or may nor be in effect.
322                  * These usually indicate SQL syntax errors that need to be resolved.
323                  */
324
325                 if ($result === false) {
326                         logger('dba: ' . printable($sql) . ' returned false.' . "\n" . $this->error);
327                         if (file_exists('dbfail.out')) {
328                                 file_put_contents('dbfail.out', datetime_convert() . "\n" . printable($sql) . ' returned false' . "\n" . $this->error . "\n", FILE_APPEND);
329                         }
330                 }
331
332                 if (is_bool($result)) {
333                         return $result;
334                 }
335                 if ($onlyquery) {
336                         $this->result = $result;
337                         return true;
338                 }
339
340                 $r = array();
341                 switch ($this->driver) {
342                         case 'pdo':
343                                 while ($x = $result->fetch(PDO::FETCH_ASSOC)) {
344                                         $r[] = $x;
345                                 }
346                                 $result->closeCursor();
347                                 break;
348                         case 'mysqli':
349                                 while ($x = $result->fetch_array(MYSQLI_ASSOC)) {
350                                         $r[] = $x;
351                                 }
352                                 $result->free_result();
353                                 break;
354                         case 'mysql':
355                                 while ($x = mysql_fetch_array($result, MYSQL_ASSOC)) {
356                                         $r[] = $x;
357                                 }
358                                 mysql_free_result($result);
359                                 break;
360                 }
361
362                 // PDO doesn't return "true" on successful operations - like mysqli does
363                 // Emulate this behaviour by checking if the query returned data and had columns
364                 // This should be reliable enough
365                 if (($this->driver == 'pdo') AND (count($r) == 0) AND ($columns == 0)) {
366                         return true;
367                 }
368
369                 //$a->save_timestamp($stamp1, "database");
370
371                 if ($this->debug) {
372                         logger('dba: ' . printable(print_r($r, true)));
373                 }
374                 return($r);
375         }
376
377         public function dbg($dbg) {
378                 $this->debug = $dbg;
379         }
380
381         public function escape($str) {
382                 if ($this->db && $this->connected) {
383                         switch ($this->driver) {
384                                 case 'pdo':
385                                         return substr(@$this->db->quote($str, PDO::PARAM_STR), 1, -1);
386                                 case 'mysqli':
387                                         return @$this->db->real_escape_string($str);
388                                 case 'mysql':
389                                         return @mysql_real_escape_string($str,$this->db);
390                         }
391                 }
392         }
393
394         function connected() {
395                 switch ($this->driver) {
396                         case 'pdo':
397                                 // Not sure if this really is working like expected
398                                 $connected = ($this->db->getAttribute(PDO::ATTR_CONNECTION_STATUS) != "");
399                                 break;
400                         case 'mysqli':
401                                 $connected = $this->db->ping();
402                                 break;
403                         case 'mysql':
404                                 $connected = mysql_ping($this->db);
405                                 break;
406                 }
407                 return $connected;
408         }
409
410         function insert_id() {
411                 switch ($this->driver) {
412                         case 'pdo':
413                                 $id = $this->db->lastInsertId();
414                                 break;
415                         case 'mysqli':
416                                 $id = $this->db->insert_id;
417                                 break;
418                         case 'mysql':
419                                 $id = mysql_insert_id($this->db);
420                                 break;
421                 }
422                 return $id;
423         }
424
425         function __destruct() {
426                 if ($this->db) {
427                         switch ($this->driver) {
428                                 case 'pdo':
429                                         $this->db = null;
430                                         break;
431                                 case 'mysqli':
432                                         $this->db->close();
433                                         break;
434                                 case 'mysql':
435                                         mysql_close($this->db);
436                                         break;
437                         }
438                 }
439         }
440
441         /**
442          * @brief Replaces ANY_VALUE() function by MIN() function,
443          *  if the database server does not support ANY_VALUE().
444          *
445          * Considerations for Standard SQL, or MySQL with ONLY_FULL_GROUP_BY (default since 5.7.5).
446          * ANY_VALUE() is available from MySQL 5.7.5 https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html
447          * A standard fall-back is to use MIN().
448          *
449          * @param string $sql An SQL string without the values
450          * @return string The input SQL string modified if necessary.
451          */
452         public function any_value_fallback($sql) {
453                 $server_info = $this->server_info();
454                 if (version_compare($server_info, '5.7.5', '<') ||
455                         (stripos($server_info, 'MariaDB') !== false)) {
456                         $sql = str_ireplace('ANY_VALUE(', 'MIN(', $sql);
457                 }
458                 return $sql;
459         }
460
461         /**
462          * @brief Replaces the ? placeholders with the parameters in the $args array
463          *
464          * @param string $sql SQL query
465          * @param array $args The parameters that are to replace the ? placeholders
466          * @return string The replaced SQL query
467          */
468         static private function replace_parameters($sql, $args) {
469                 $offset = 0;
470                 foreach ($args AS $param => $value) {
471                         if (is_int($args[$param]) OR is_float($args[$param])) {
472                                 $replace = intval($args[$param]);
473                         } else {
474                                 $replace = "'".self::$dbo->escape($args[$param])."'";
475                         }
476
477                         $pos = strpos($sql, '?', $offset);
478                         if ($pos !== false) {
479                                 $sql = substr_replace($sql, $replace, $pos, 1);
480                         }
481                         $offset = $pos + strlen($replace);
482                 }
483                 return $sql;
484         }
485
486         /**
487          * @brief Executes a prepared statement that returns data
488          * @usage Example: $r = p("SELECT * FROM `item` WHERE `guid` = ?", $guid);
489          * @param string $sql SQL statement
490          * @return object statement object
491          */
492         static public function p($sql) {
493                 $a = get_app();
494
495                 $stamp1 = microtime(true);
496
497                 $args = func_get_args();
498                 unset($args[0]);
499
500                 // When the second function parameter is an array then use this as the parameter array
501                 if ((count($args) > 0) AND (is_array($args[1]))) {
502                         $params = $args[1];
503                 } else {
504                         $params = $args;
505                 }
506
507                 // Renumber the array keys to be sure that they fit
508                 $i = 0;
509                 $args = array();
510                 foreach ($params AS $param) {
511                         $args[++$i] = $param;
512                 }
513
514                 if (!self::$dbo OR !self::$dbo->connected) {
515                         return false;
516                 }
517
518                 if (substr_count($sql, '?') != count($args)) {
519                         // Question: Should we continue or stop the query here?
520                         logger('Parameter mismatch. Query "'.$sql.'" - Parameters '.print_r($args, true), LOGGER_DEBUG);
521                 }
522
523                 $sql = self::$dbo->any_value_fallback($sql);
524
525                 if (x($a->config,'system') && x($a->config['system'], 'db_callstack')) {
526                         $sql = "/*".$a->callstack()." */ ".$sql;
527                 }
528
529                 self::$dbo->error = '';
530                 self::$dbo->errorno = 0;
531
532                 switch (self::$dbo->driver) {
533                         case 'pdo':
534                                 if (!$stmt = self::$dbo->db->prepare($sql)) {
535                                         $errorInfo = self::$dbo->db->errorInfo();
536                                         self::$dbo->error = $errorInfo[2];
537                                         self::$dbo->errorno = $errorInfo[1];
538                                         $retval = false;
539                                         break;
540                                 }
541
542                                 foreach ($args AS $param => $value) {
543                                         $stmt->bindParam($param, $args[$param]);
544                                 }
545
546                                 if (!$stmt->execute()) {
547                                         $errorInfo = $stmt->errorInfo();
548                                         self::$dbo->error = $errorInfo[2];
549                                         self::$dbo->errorno = $errorInfo[1];
550                                         $retval = false;
551                                 } else {
552                                         $retval = $stmt;
553                                 }
554                                 break;
555                         case 'mysqli':
556                                 $stmt = self::$dbo->db->stmt_init();
557
558                                 if (!$stmt->prepare($sql)) {
559                                         self::$dbo->error = $stmt->error;
560                                         self::$dbo->errorno = $stmt->errno;
561                                         $retval = false;
562                                         break;
563                                 }
564
565                                 $params = '';
566                                 $values = array();
567                                 foreach ($args AS $param => $value) {
568                                         if (is_int($args[$param])) {
569                                                 $params .= 'i';
570                                         } elseif (is_float($args[$param])) {
571                                                 $params .= 'd';
572                                         } elseif (is_string($args[$param])) {
573                                                 $params .= 's';
574                                         } else {
575                                                 $params .= 'b';
576                                         }
577                                         $values[] = &$args[$param];
578                                 }
579
580                                 if (count($values) > 0) {
581                                         array_unshift($values, $params);
582                                         call_user_func_array(array($stmt, 'bind_param'), $values);
583                                 }
584
585                                 if (!$stmt->execute()) {
586                                         self::$dbo->error = self::$dbo->db->error;
587                                         self::$dbo->errorno = self::$dbo->db->errno;
588                                         $retval = false;
589                                 } else {
590                                         $stmt->store_result();
591                                         $retval = $stmt;
592                                 }
593                                 break;
594                         case 'mysql':
595                                 // For the old "mysql" functions we cannot use prepared statements
596                                 $retval = mysql_query(self::replace_parameters($sql, $args), self::$dbo->db);
597                                 if (mysql_errno(self::$dbo->db)) {
598                                         self::$dbo->error = mysql_error(self::$dbo->db);
599                                         self::$dbo->errorno = mysql_errno(self::$dbo->db);
600                                 }
601                                 break;
602                 }
603
604                 if (self::$dbo->errorno != 0) {
605                         logger('DB Error '.self::$dbo->errorno.': '.self::$dbo->error."\n".self::replace_parameters($sql, $args));
606                 }
607
608                 $a->save_timestamp($stamp1, 'database');
609
610                 if (x($a->config,'system') && x($a->config['system'], 'db_log')) {
611
612                         $stamp2 = microtime(true);
613                         $duration = (float)($stamp2 - $stamp1);
614
615                         if (($duration > $a->config["system"]["db_loglimit"])) {
616                                 $duration = round($duration, 3);
617                                 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
618
619                                 @file_put_contents($a->config["system"]["db_log"], datetime_convert()."\t".$duration."\t".
620                                                 basename($backtrace[1]["file"])."\t".
621                                                 $backtrace[1]["line"]."\t".$backtrace[2]["function"]."\t".
622                                                 substr(self::replace_parameters($sql, $args), 0, 2000)."\n", FILE_APPEND);
623                         }
624                 }
625                 return $retval;
626         }
627
628         /**
629          * @brief Executes a prepared statement like UPDATE or INSERT that doesn't return data
630          *
631          * @param string $sql SQL statement
632          * @return boolean Was the query successfull? False is returned only if an error occurred
633          */
634         static public function e($sql) {
635                 $a = get_app();
636
637                 $stamp = microtime(true);
638
639                 $args = func_get_args();
640
641                 $stmt = call_user_func_array('self::p', $args);
642
643                 if (is_bool($stmt)) {
644                         $retval = $stmt;
645                 } elseif (is_object($stmt)) {
646                         $retval = true;
647                 } else {
648                         $retval = false;
649                 }
650
651                 self::close($stmt);
652
653                 $a->save_timestamp($stamp, "database_write");
654
655                 return $retval;
656         }
657
658         /**
659          * @brief Check if data exists
660          *
661          * @param string $sql SQL statement
662          * @return boolean Are there rows for that query?
663          */
664         static public function exists($sql) {
665                 $args = func_get_args();
666
667                 $stmt = call_user_func_array('self::p', $args);
668
669                 if (is_bool($stmt)) {
670                         $retval = $stmt;
671                 } else {
672                         $retval = (self::num_rows($stmt) > 0);
673                 }
674
675                 self::close($stmt);
676
677                 return $retval;
678         }
679
680         /**
681          * @brief Fetches the first row
682          *
683          * @param string $sql SQL statement
684          * @return array first row of query
685          */
686         static public function fetch_first($sql) {
687                 $args = func_get_args();
688
689                 $stmt = call_user_func_array('self::p', $args);
690
691                 if (is_bool($stmt)) {
692                         $retval = $stmt;
693                 } else {
694                         $retval = self::fetch($stmt);
695                 }
696
697                 self::close($stmt);
698
699                 return $retval;
700         }
701
702         /**
703          * @brief Returns the number of rows of a statement
704          *
705          * @param object Statement object
706          * @return int Number of rows
707          */
708         static public function num_rows($stmt) {
709                 switch (self::$dbo->driver) {
710                         case 'pdo':
711                                 return $stmt->rowCount();
712                         case 'mysqli':
713                                 return $stmt->num_rows;
714                         case 'mysql':
715                                 return mysql_num_rows($stmt);
716                 }
717                 return 0;
718         }
719
720         /**
721          * @brief Fetch a single row
722          *
723          * @param object $stmt statement object
724          * @return array current row
725          */
726         static public function fetch($stmt) {
727                 if (!is_object($stmt)) {
728                         return false;
729                 }
730
731                 switch (self::$dbo->driver) {
732                         case 'pdo':
733                                 return $stmt->fetch(PDO::FETCH_ASSOC);
734                         case 'mysqli':
735                                 // This code works, but is slow
736
737                                 // Bind the result to a result array
738                                 $cols = array();
739
740                                 $cols_num = array();
741                                 for ($x = 0; $x < $stmt->field_count; $x++) {
742                                         $cols[] = &$cols_num[$x];
743                                 }
744
745                                 call_user_func_array(array($stmt, 'bind_result'), $cols);
746
747                                 if (!$stmt->fetch()) {
748                                         return false;
749                                 }
750
751                                 // The slow part:
752                                 // We need to get the field names for the array keys
753                                 // It seems that there is no better way to do this.
754                                 $result = $stmt->result_metadata();
755                                 $fields = $result->fetch_fields();
756
757                                 $columns = array();
758                                 foreach ($cols_num AS $param => $col) {
759                                         $columns[$fields[$param]->name] = $col;
760                                 }
761                                 return $columns;
762                         case 'mysql':
763                                 return mysql_fetch_array(self::$dbo->result, MYSQL_ASSOC);
764                 }
765         }
766
767         /**
768          * @brief Insert a row into a table
769          *
770          * @param string $table Table name
771          * @param array $param parameter array
772          *
773          * @return boolean was the insert successfull?
774          */
775         static public function insert($table, $param) {
776                 $sql = "INSERT INTO `".self::$dbo->escape($table)."` (`".implode("`, `", array_keys($param))."`) VALUES (".
777                         substr(str_repeat("?, ", count($param)), 0, -2).");";
778
779                 return self::e($sql, $param);
780         }
781
782         /**
783          * @brief Build the array with the table relations
784          *
785          * The array is build from the database definitions in dbstructure.php
786          *
787          * This process must only be started once, since the value is cached.
788          */
789         static private function build_relation_data() {
790                 $definition = db_definition();
791
792                 foreach ($definition AS $table => $structure) {
793                         foreach ($structure['fields'] AS $field => $field_struct) {
794                                 if (isset($field_struct['relation'])) {
795                                         foreach ($field_struct['relation'] AS $rel_table => $rel_field) {
796                                                 self::$relation[$rel_table][$rel_field][$table][] = $field;
797                                         }
798                                 }
799                         }
800                 }
801         }
802
803         /**
804          * @brief Insert a row into a table
805          *
806          * @param string $table Table name
807          * @param array $param parameter array
808          * @param boolean $in_commit Internal use: Only do a commit after the last delete
809          * @param array $callstack Internal use: prevent endless loops
810          *
811          * @return boolean|array was the delete successfull? When $in_commit is set: deletion data
812          */
813         static public function delete($table, $param, $in_commit = false, &$callstack = array()) {
814
815                 $commands = array();
816
817                 // Create a key for the loop prevention
818                 $key = $table.':'.implode(':', array_keys($param)).':'.implode(':', $param);
819
820                 // We quit when this key already exists in the callstack.
821                 if (isset($callstack[$key])) {
822                         return $commands;
823                 }
824
825                 $callstack[$key] = true;
826
827                 $table = self::$dbo->escape($table);
828
829                 $commands[$key] = array('table' => $table, 'param' => $param);
830
831                 // To speed up the whole process we cache the table relations
832                 if (count(self::$relation) == 0) {
833                         self::build_relation_data();
834                 }
835
836                 // Is there a relation entry for the table?
837                 if (isset(self::$relation[$table])) {
838                         // We only allow a simple "one field" relation.
839                         $field = array_keys(self::$relation[$table])[0];
840                         $rel_def = array_values(self::$relation[$table])[0];
841
842                         // Create a key for preventing double queries
843                         $qkey = $field.'-'.$table.':'.implode(':', array_keys($param)).':'.implode(':', $param);
844
845                         // When the search field is the relation field, we don't need to fetch the rows
846                         // This is useful when the leading record is already deleted in the frontend but the rest is done in the backend
847                         if ((count($param) == 1) AND ($field == array_keys($param)[0])) {
848                                 foreach ($rel_def AS $rel_table => $rel_fields) {
849                                         foreach ($rel_fields AS $rel_field) {
850                                                 $retval = self::delete($rel_table, array($rel_field => array_values($param)[0]), true, $callstack);
851                                                 $commands = array_merge($commands, $retval);
852                                         }
853                                 }
854                         // We quit when this key already exists in the callstack.
855                         } elseif (!isset($callstack[$qkey])) {
856
857                                 $callstack[$qkey] = true;
858
859                                 // Fetch all rows that are to be deleted
860                                 $sql = "SELECT ".self::$dbo->escape($field)." FROM `".$table."` WHERE `".
861                                 implode("` = ? AND `", array_keys($param))."` = ?";
862
863                                 $data = self::p($sql, $param);
864                                 while ($row = self::fetch($data)) {
865                                         // Now we accumulate the delete commands
866                                         $retval = self::delete($table, array($field => $row[$field]), true, $callstack);
867                                         $commands = array_merge($commands, $retval);
868                                 }
869
870                                 // Since we had split the delete command we don't need the original command anymore
871                                 unset($commands[$key]);
872                         }
873                 }
874
875                 if (!$in_commit) {
876                         // Now we finalize the process
877                         self::p("COMMIT");
878                         self::p("START TRANSACTION");
879
880                         $compacted = array();
881                         $counter = array();
882                         foreach ($commands AS $command) {
883                                 if (count($command['param']) > 1) {
884                                         $sql = "DELETE FROM `".$command['table']."` WHERE `".
885                                                 implode("` = ? AND `", array_keys($command['param']))."` = ?";
886
887                                         logger(dba::replace_parameters($sql, $command['param']), LOGGER_DATA);
888
889                                         if (!self::e($sql, $param)) {
890                                                 self::p("ROLLBACK");
891                                                 return false;
892                                         }
893                                 } else {
894                                         $key_table = $command['table'];
895                                         $key_param = array_keys($command['param'])[0];
896                                         $value = array_values($command['param'])[0];
897
898                                         // Split the SQL queries in chunks of 100 values
899                                         // We do the $i stuff here to make the code better readable
900                                         $i = $counter[$key_table][$key_param];
901                                         if (count($compacted[$key_table][$key_param][$i]) > 100) {
902                                                 ++$i;
903                                         }
904
905                                         $compacted[$key_table][$key_param][$i][$value] = $value;
906                                         $counter[$key_table][$key_param] = $i;
907                                 }
908                         }
909                         foreach ($compacted AS $table => $values) {
910                                 foreach ($values AS $field => $field_value_list) {
911                                         foreach ($field_value_list AS $field_values) {
912                                                 $sql = "DELETE FROM `".$table."` WHERE `".$field."` IN (".
913                                                         substr(str_repeat("?, ", count($field_values)), 0, -2).");";
914
915                                                 logger(dba::replace_parameters($sql, $field_values), LOGGER_DATA);
916
917                                                 if (!self::e($sql, $param)) {
918                                                         self::p("ROLLBACK");
919                                                         return false;
920                                                 }
921                                         }
922                                 }
923                         }
924                         self::p("COMMIT");
925                         return true;
926                 }
927
928                 return $commands;
929         }
930
931         /**
932          * @brief Updates rows
933          *
934          * Updates rows in the database. When $old_fields is set to an array,
935          * the system will only do an update if the fields in that array changed.
936          *
937          * Attention:
938          * Only the values in $old_fields are compared.
939          * This is an intentional behaviour.
940          *
941          * Example:
942          * We include the timestamp field in $fields but not in $old_fields.
943          * Then the row will only get the new timestamp when the other fields had changed.
944          *
945          * When $old_fields is set to a boolean value the system will do this compare itself.
946          * When $old_fields is set to "true" the system will do an insert if the row doesn't exists.
947          *
948          * Attention:
949          * Only set $old_fields to a boolean value when you are sure that you will update a single row.
950          * When you set $old_fields to "true" then $fields must contain all relevant fields!
951          *
952          * @param string $table Table name
953          * @param array $fields contains the fields that are updated
954          * @param array $condition condition array with the key values
955          * @param array|boolean $old_fields array with the old field values that are about to be replaced
956          *
957          * @return boolean was the update successfull?
958          */
959         static public function update($table, $fields, $condition, $old_fields = array()) {
960
961                 /** @todo We may use MySQL specific functions here:
962                  * INSERT INTO `config` (`cat`, `k`, `v`) VALUES ('%s', '%s', '%s') ON DUPLICATE KEY UPDATE `v` = '%s'"
963                  * But I think that it doesn't make sense here.
964                 */
965
966                 $table = self::$dbo->escape($table);
967
968                 if (is_bool($old_fields)) {
969                         $sql = "SELECT * FROM `".$table."` WHERE `".
970                         implode("` = ? AND `", array_keys($condition))."` = ? LIMIT 1";
971
972                         $params = array();
973                         foreach ($condition AS $value) {
974                                 $params[] = $value;
975                         }
976
977                         $do_insert = $old_fields;
978
979                         $old_fields = self::fetch_first($sql, $params);
980                         if (is_bool($old_fields)) {
981                                 if ($do_insert) {
982                                         return self::insert($table, $fields);
983                                 }
984                                 $old_fields = array();
985                         }
986                 }
987
988                 $do_update = (count($old_fields) == 0);
989
990                 foreach ($old_fields AS $fieldname => $content) {
991                         if (isset($fields[$fieldname])) {
992                                 if ($fields[$fieldname] == $content) {
993                                         unset($fields[$fieldname]);
994                                 } else {
995                                         $do_update = true;
996                                 }
997                         }
998                 }
999
1000                 if (!$do_update OR (count($fields) == 0)) {
1001                         return true;
1002                 }
1003
1004                 $sql = "UPDATE `".$table."` SET `".
1005                         implode("` = ?, `", array_keys($fields))."` = ? WHERE `".
1006                         implode("` = ? AND `", array_keys($condition))."` = ?";
1007
1008                 $params = array();
1009                 foreach ($fields AS $value) {
1010                         $params[] = $value;
1011                 }
1012                 foreach ($condition AS $value) {
1013                         $params[] = $value;
1014                 }
1015
1016                 return self::e($sql, $params);
1017         }
1018
1019         /**
1020          * @brief Select rows from a table
1021          *
1022          * @param string $table Table name
1023          * @param array $fields array of selected fields
1024          * @param array $condition array of fields for condition
1025          * @param array $params array of several parameters
1026          *
1027          * @return boolean|object If "limit" is equal "1" only a single row is returned, else a query object is returned
1028          *
1029          * Example:
1030          * $table = "item";
1031          * $fields = array("id", "uri", "uid", "network");
1032          * $condition = array("uid" => 1, "network" => 'dspr');
1033          * $params = array("order" => array("id", "received" => true), "limit" => 1);
1034          *
1035          * $data = dba::select($table, $fields, $condition, $params);
1036          */
1037         static public function select($table, $fields = array(), $condition = array(), $params = array()) {
1038                 if ($table == '') {
1039                         return false;
1040                 }
1041
1042                 if (count($fields) > 0) {
1043                         $select_fields = "`".implode("`, `", array_values($fields))."`";
1044                 } else {
1045                         $select_fields = "*";
1046                 }
1047
1048                 if (count($condition) > 0) {
1049                         $condition_string = " WHERE `".implode("` = ? AND `", array_keys($condition))."` = ?";
1050                 } else {
1051                         $condition_string = "";
1052                 }
1053
1054                 $param_string = '';
1055                 $single_row = false;
1056
1057                 if (isset($params['order'])) {
1058                         $param_string .= " ORDER BY ";
1059                         foreach ($params['order'] AS $fields => $order) {
1060                                 if (!is_int($fields)) {
1061                                         $param_string .= "`".$fields."` ".($order ? "ASC" : "DESC").", ";
1062                                 } else {
1063                                         $param_string .= "`".$order."`, ";
1064                                 }
1065                         }
1066                         $param_string = substr($param_string, 0, -2);
1067                 }
1068
1069                 if (isset($params['limit'])) {
1070                         if (is_int($params['limit'])) {
1071                                 $param_string .= " LIMIT ".$params['limit'];
1072                                 $single_row =($params['limit'] == 1);
1073                         }
1074                 }
1075
1076                 $sql = "SELECT ".$select_fields." FROM `".$table."`".$condition_string.$param_string;
1077
1078                 $result = self::p($sql, $condition);
1079
1080                 if (is_bool($result) OR !$single_row) {
1081                         return $result;
1082                 } else {
1083                         $row = self::fetch($result);
1084                         self::close($result);
1085                         return $row;
1086                 }
1087         }
1088
1089         /**
1090          * @brief Closes the current statement
1091          *
1092          * @param object $stmt statement object
1093          * @return boolean was the close successfull?
1094          */
1095         static public function close($stmt) {
1096                 if (!is_object($stmt)) {
1097                         return false;
1098                 }
1099
1100                 switch (self::$dbo->driver) {
1101                         case 'pdo':
1102                                 return $stmt->closeCursor();
1103                         case 'mysqli':
1104                                 return $stmt->free_result();
1105                                 return $stmt->close();
1106                         case 'mysql':
1107                                 return mysql_free_result($stmt);
1108                 }
1109         }
1110 }
1111
1112 function printable($s) {
1113         $s = preg_replace("~([\x01-\x08\x0E-\x0F\x10-\x1F\x7F-\xFF])~",".", $s);
1114         $s = str_replace("\x00",'.',$s);
1115         if (x($_SERVER,'SERVER_NAME')) {
1116                 $s = escape_tags($s);
1117         }
1118         return $s;
1119 }
1120
1121 // Procedural functions
1122 function dbg($state) {
1123         global $db;
1124
1125         if ($db) {
1126                 $db->dbg($state);
1127         }
1128 }
1129
1130 function dbesc($str) {
1131         global $db;
1132
1133         if ($db && $db->connected) {
1134                 return($db->escape($str));
1135         } else {
1136                 return(str_replace("'","\\'",$str));
1137         }
1138 }
1139
1140 // Function: q($sql,$args);
1141 // Description: execute SQL query with printf style args.
1142 // Example: $r = q("SELECT * FROM `%s` WHERE `uid` = %d",
1143 //                   'user', 1);
1144 function q($sql) {
1145         global $db;
1146         $args = func_get_args();
1147         unset($args[0]);
1148
1149         if ($db && $db->connected) {
1150                 $sql = $db->any_value_fallback($sql);
1151                 $stmt = @vsprintf($sql,$args); // Disabled warnings
1152                 //logger("dba: q: $stmt", LOGGER_ALL);
1153                 if ($stmt === false)
1154                         logger('dba: vsprintf error: ' . print_r(debug_backtrace(),true), LOGGER_DEBUG);
1155
1156                 $db->log_index($stmt);
1157
1158                 return $db->q($stmt);
1159         }
1160
1161         /**
1162          *
1163          * This will happen occasionally trying to store the
1164          * session data after abnormal program termination
1165          *
1166          */
1167         logger('dba: no database: ' . print_r($args,true));
1168         return false;
1169 }
1170
1171 /**
1172  * @brief Performs a query with "dirty reads"
1173  *
1174  * By doing dirty reads (reading uncommitted data) no locks are performed
1175  * This function can be used to fetch data that doesn't need to be reliable.
1176  *
1177  * @param $args Query parameters (1 to N parameters of different types)
1178  * @return array Query array
1179  */
1180 function qu($sql) {
1181         global $db;
1182
1183         $args = func_get_args();
1184         unset($args[0]);
1185
1186         if ($db && $db->connected) {
1187                 $sql = $db->any_value_fallback($sql);
1188                 $stmt = @vsprintf($sql,$args); // Disabled warnings
1189                 if ($stmt === false)
1190                         logger('dba: vsprintf error: ' . print_r(debug_backtrace(),true), LOGGER_DEBUG);
1191
1192                 $db->log_index($stmt);
1193
1194                 $db->q("SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;");
1195                 $retval = $db->q($stmt);
1196                 $db->q("SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;");
1197                 return $retval;
1198         }
1199
1200         /**
1201          *
1202          * This will happen occasionally trying to store the
1203          * session data after abnormal program termination
1204          *
1205          */
1206         logger('dba: no database: ' . print_r($args,true));
1207         return false;
1208 }
1209
1210 /**
1211  *
1212  * Raw db query, no arguments
1213  *
1214  */
1215 function dbq($sql) {
1216         global $db;
1217
1218         if ($db && $db->connected) {
1219                 $ret = $db->q($sql);
1220         } else {
1221                 $ret = false;
1222         }
1223         return $ret;
1224 }
1225
1226 // Caller is responsible for ensuring that any integer arguments to
1227 // dbesc_array are actually integers and not malformed strings containing
1228 // SQL injection vectors. All integer array elements should be specifically
1229 // cast to int to avoid trouble.
1230 function dbesc_array_cb(&$item, $key) {
1231         if (is_string($item))
1232                 $item = dbesc($item);
1233 }
1234
1235 function dbesc_array(&$arr) {
1236         if (is_array($arr) && count($arr)) {
1237                 array_walk($arr,'dbesc_array_cb');
1238         }
1239 }
1240
1241 function dba_timer() {
1242         return microtime(true);
1243 }