]> git.mxchange.org Git - friendica.git/blob - src/Database/DBA.php
Merge branch 'bug/phpinfo-accessible-hotfix' into 2020.09-rc
[friendica.git] / src / Database / DBA.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\DI;
25 use mysqli;
26 use mysqli_result;
27 use mysqli_stmt;
28 use PDO;
29 use PDOStatement;
30
31 /**
32  * This class is for the low level database stuff that does driver specific things.
33  */
34 class DBA
35 {
36         /**
37          * Lowest possible date value
38          */
39         const NULL_DATE     = '0001-01-01';
40         /**
41          * Lowest possible datetime value
42          */
43         const NULL_DATETIME = '0001-01-01 00:00:00';
44
45         public static function connect()
46         {
47                 return DI::dba()->connect();
48         }
49
50         /**
51          * Disconnects the current database connection
52          */
53         public static function disconnect()
54         {
55                 DI::dba()->disconnect();
56         }
57
58         /**
59          * Perform a reconnect of an existing database connection
60          */
61         public static function reconnect()
62         {
63                 return DI::dba()->reconnect();
64         }
65
66         /**
67          * Return the database object.
68          * @return PDO|mysqli
69          */
70         public static function getConnection()
71         {
72                 return DI::dba()->getConnection();
73         }
74
75         /**
76          * Returns the MySQL server version string
77          *
78          * This function discriminate between the deprecated mysql API and the current
79          * object-oriented mysqli API. Example of returned string: 5.5.46-0+deb8u1
80          *
81          * @return string
82          */
83         public static function serverInfo()
84         {
85                 return DI::dba()->serverInfo();
86         }
87
88         /**
89          * Returns the selected database name
90          *
91          * @return string
92          * @throws \Exception
93          */
94         public static function databaseName()
95         {
96                 return DI::dba()->databaseName();
97         }
98
99         /**
100          * Escape all SQL unsafe data
101          *
102          * @param string $str
103          * @return string escaped string
104          */
105         public static function escape($str)
106         {
107                 return DI::dba()->escape($str);
108         }
109
110         /**
111          * Checks if the database is connected
112          *
113          * @return boolean is the database connected?
114          */
115         public static function connected()
116         {
117                 return DI::dba()->connected();
118         }
119
120         /**
121          * Replaces ANY_VALUE() function by MIN() function,
122          * if the database server does not support ANY_VALUE().
123          *
124          * Considerations for Standard SQL, or MySQL with ONLY_FULL_GROUP_BY (default since 5.7.5).
125          * ANY_VALUE() is available from MySQL 5.7.5 https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html
126          * A standard fall-back is to use MIN().
127          *
128          * @param string $sql An SQL string without the values
129          * @return string The input SQL string modified if necessary.
130          */
131         public static function anyValueFallback($sql)
132         {
133                 return DI::dba()->anyValueFallback($sql);
134         }
135
136         /**
137          * beautifies the query - useful for "SHOW PROCESSLIST"
138          *
139          * This is safe when we bind the parameters later.
140          * The parameter values aren't part of the SQL.
141          *
142          * @param string $sql An SQL string without the values
143          * @return string The input SQL string modified if necessary.
144          */
145         public static function cleanQuery($sql)
146         {
147                 $search = ["\t", "\n", "\r", "  "];
148                 $replace = [' ', ' ', ' ', ' '];
149                 do {
150                         $oldsql = $sql;
151                         $sql = str_replace($search, $replace, $sql);
152                 } while ($oldsql != $sql);
153
154                 return $sql;
155         }
156
157         /**
158          * Convert parameter array to an universal form
159          * @param array $args Parameter array
160          * @return array universalized parameter array
161          */
162         public static function getParam($args)
163         {
164                 unset($args[0]);
165
166                 // When the second function parameter is an array then use this as the parameter array
167                 if ((count($args) > 0) && (is_array($args[1]))) {
168                         return $args[1];
169                 } else {
170                         return $args;
171                 }
172         }
173
174         /**
175          * Executes a prepared statement that returns data
176          * Example: $r = p("SELECT * FROM `item` WHERE `guid` = ?", $guid);
177          *
178          * Please only use it with complicated queries.
179          * For all regular queries please use DBA::select or DBA::exists
180          *
181          * @param string $sql SQL statement
182          * @return bool|object statement object or result object
183          * @throws \Exception
184          */
185         public static function p($sql)
186         {
187                 $params = self::getParam(func_get_args());
188
189                 return DI::dba()->p($sql, $params);
190         }
191
192         /**
193          * Executes a prepared statement like UPDATE or INSERT that doesn't return data
194          *
195          * Please use DBA::delete, DBA::insert, DBA::update, ... instead
196          *
197          * @param string $sql SQL statement
198          * @return boolean Was the query successfull? False is returned only if an error occurred
199          * @throws \Exception
200          */
201         public static function e($sql) {
202
203                 $params = self::getParam(func_get_args());
204
205                 return DI::dba()->e($sql, $params);
206         }
207
208         /**
209          * Check if data exists
210          *
211          * @param string|array $table     Table name or array [schema => table]
212          * @param array        $condition array of fields for condition
213          *
214          * @return boolean Are there rows for that condition?
215          * @throws \Exception
216          */
217         public static function exists($table, $condition)
218         {
219                 return DI::dba()->exists($table, $condition);
220         }
221
222         /**
223          * Fetches the first row
224          *
225          * Please use DBA::selectFirst or DBA::exists whenever this is possible.
226          *
227          * @param string $sql SQL statement
228          * @return array first row of query
229          * @throws \Exception
230          */
231         public static function fetchFirst($sql)
232         {
233                 $params = self::getParam(func_get_args());
234
235                 return DI::dba()->fetchFirst($sql, $params);
236         }
237
238         /**
239          * Returns the number of affected rows of the last statement
240          *
241          * @return int Number of rows
242          */
243         public static function affectedRows()
244         {
245                 return DI::dba()->affectedRows();
246         }
247
248         /**
249          * Returns the number of columns of a statement
250          *
251          * @param object Statement object
252          * @return int Number of columns
253          */
254         public static function columnCount($stmt)
255         {
256                 return DI::dba()->columnCount($stmt);
257         }
258         /**
259          * Returns the number of rows of a statement
260          *
261          * @param PDOStatement|mysqli_result|mysqli_stmt Statement object
262          * @return int Number of rows
263          */
264         public static function numRows($stmt)
265         {
266                 return DI::dba()->numRows($stmt);
267         }
268
269         /**
270          * Fetch a single row
271          *
272          * @param mixed $stmt statement object
273          * @return array current row
274          */
275         public static function fetch($stmt)
276         {
277                 return DI::dba()->fetch($stmt);
278         }
279
280         /**
281          * Insert a row into a table
282          *
283          * @param string|array $table               Table name or array [schema => table]
284          * @param array        $param               parameter array
285          * @param bool         $on_duplicate_update Do an update on a duplicate entry
286          *
287          * @return boolean was the insert successful?
288          * @throws \Exception
289          */
290         public static function insert($table, $param, $on_duplicate_update = false)
291         {
292                 return DI::dba()->insert($table, $param, $on_duplicate_update);
293         }
294
295         /**
296          * Inserts a row with the provided data in the provided table.
297          * If the data corresponds to an existing row through a UNIQUE or PRIMARY index constraints, it updates the row instead.
298          *
299          * @param string|array $table Table name or array [schema => table]
300          * @param array        $param parameter array
301          *
302          * @return boolean was the insert successful?
303          * @throws \Exception
304          */
305         public static function replace($table, $param)
306         {
307                 return DI::dba()->replace($table, $param);
308         }
309
310         /**
311          * Fetch the id of the last insert command
312          *
313          * @return integer Last inserted id
314          */
315         public static function lastInsertId()
316         {
317                 return DI::dba()->lastInsertId();
318         }
319
320         /**
321          * Locks a table for exclusive write access
322          *
323          * This function can be extended in the future to accept a table array as well.
324          *
325          * @param string|array $table Table name or array [schema => table]
326          *
327          * @return boolean was the lock successful?
328          * @throws \Exception
329          */
330         public static function lock($table)
331         {
332                 return DI::dba()->lock($table);
333         }
334
335         /**
336          * Unlocks all locked tables
337          *
338          * @return boolean was the unlock successful?
339          * @throws \Exception
340          */
341         public static function unlock()
342         {
343                 return DI::dba()->unlock();
344         }
345
346         /**
347          * Starts a transaction
348          *
349          * @return boolean Was the command executed successfully?
350          */
351         public static function transaction()
352         {
353                 return DI::dba()->transaction();
354         }
355
356         /**
357          * Does a commit
358          *
359          * @return boolean Was the command executed successfully?
360          */
361         public static function commit()
362         {
363                 return DI::dba()->commit();
364         }
365
366         /**
367          * Does a rollback
368          *
369          * @return boolean Was the command executed successfully?
370          */
371         public static function rollback()
372         {
373                 return DI::dba()->rollback();
374         }
375
376         /**
377          * Delete a row from a table
378          *
379          * @param string|array $table      Table name
380          * @param array        $conditions Field condition(s)
381          * @param array        $options
382          *                           - cascade: If true we delete records in other tables that depend on the one we're deleting through
383          *                           relations (default: true)
384          *
385          * @return boolean was the delete successful?
386          * @throws \Exception
387          */
388         public static function delete($table, array $conditions, array $options = [])
389         {
390                 return DI::dba()->delete($table, $conditions, $options);
391         }
392
393         /**
394          * Updates rows in the database.
395          *
396          * When $old_fields is set to an array,
397          * the system will only do an update if the fields in that array changed.
398          *
399          * Attention:
400          * Only the values in $old_fields are compared.
401          * This is an intentional behaviour.
402          *
403          * Example:
404          * We include the timestamp field in $fields but not in $old_fields.
405          * Then the row will only get the new timestamp when the other fields had changed.
406          *
407          * When $old_fields is set to a boolean value the system will do this compare itself.
408          * When $old_fields is set to "true" the system will do an insert if the row doesn't exists.
409          *
410          * Attention:
411          * Only set $old_fields to a boolean value when you are sure that you will update a single row.
412          * When you set $old_fields to "true" then $fields must contain all relevant fields!
413          *
414          * @param string|array  $table      Table name or array [schema => table]
415          * @param array         $fields     contains the fields that are updated
416          * @param array         $condition  condition array with the key values
417          * @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate)
418          *
419          * @return boolean was the update successfull?
420          * @throws \Exception
421          */
422         public static function update($table, $fields, $condition, $old_fields = [])
423         {
424                 return DI::dba()->update($table, $fields, $condition, $old_fields);
425         }
426
427         /**
428          * Retrieve a single record from a table and returns it in an associative array
429          *
430          * @param string|array $table     Table name or array [schema => table]
431          * @param array        $fields
432          * @param array        $condition
433          * @param array        $params
434          * @return bool|array
435          * @throws \Exception
436          * @see   self::select
437          */
438         public static function selectFirst($table, array $fields = [], array $condition = [], $params = [])
439         {
440                 return DI::dba()->selectFirst($table, $fields, $condition, $params);
441         }
442
443         /**
444          * Select rows from a table and fills an array with the data
445          *
446          * @param string|array $table     Table name or array [schema => table]
447          * @param array        $fields    Array of selected fields, empty for all
448          * @param array        $condition Array of fields for condition
449          * @param array        $params    Array of several parameters
450          *
451          * @return array Data array
452          * @throws \Exception
453          * @see   self::select
454          */
455         public static function selectToArray($table, array $fields = [], array $condition = [], array $params = [])
456         {
457                 return DI::dba()->selectToArray($table, $fields, $condition, $params);
458         }
459
460         /**
461          * Select rows from a table
462          *
463          * @param string|array $table     Table name or array [schema => table]
464          * @param array        $fields    Array of selected fields, empty for all
465          * @param array        $condition Array of fields for condition
466          * @param array        $params    Array of several parameters
467          *
468          * @return boolean|object
469          *
470          * Example:
471          * $table = "item";
472          * $fields = array("id", "uri", "uid", "network");
473          *
474          * $condition = array("uid" => 1, "network" => 'dspr');
475          * or:
476          * $condition = array("`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr');
477          *
478          * $params = array("order" => array("id", "received" => true), "limit" => 10);
479          *
480          * $data = DBA::select($table, $fields, $condition, $params);
481          * @throws \Exception
482          */
483         public static function select($table, array $fields = [], array $condition = [], array $params = [])
484         {
485                 return DI::dba()->select($table, $fields, $condition, $params);
486         }
487
488         /**
489          * Counts the rows from a table satisfying the provided condition
490          *
491          * @param string|array $table     Table name or array [schema => table]
492          * @param array        $condition array of fields for condition
493          * @param array        $params    Array of several parameters
494          *
495          * @return int
496          *
497          * Example:
498          * $table = "item";
499          *
500          * $condition = ["uid" => 1, "network" => 'dspr'];
501          * or:
502          * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
503          *
504          * $count = DBA::count($table, $condition);
505          * @throws \Exception
506          */
507         public static function count($table, array $condition = [], array $params = [])
508         {
509                 return DI::dba()->count($table, $condition, $params);
510         }
511
512         /**
513          * Build the table query substring from one or more tables, with or without a schema.
514          *
515          * Expected formats:
516          * - table
517          * - [table1, table2, ...]
518          * - [schema1 => table1, schema2 => table2, table3, ...]
519          *
520          * @param string|array $tables
521          * @return string
522          */
523         public static function buildTableString($tables)
524         {
525                 if (is_string($tables)) {
526                         $tables = [$tables];
527                 }
528
529                 $quotedTables = [];
530
531                 foreach ($tables as $schema => $table) {
532                         if (is_numeric($schema)) {
533                                 $quotedTables[] = self::quoteIdentifier($table);
534                         } else {
535                                 $quotedTables[] = self::quoteIdentifier($schema) . '.' . self::quoteIdentifier($table);
536                         }
537                 }
538
539                 return implode(', ', $quotedTables);
540         }
541
542         /**
543          * Escape an identifier (table or field name)
544          *
545          * @param $identifier
546          * @return string
547          */
548         public static function quoteIdentifier($identifier)
549         {
550                 return '`' . str_replace('`', '``', $identifier) . '`';
551         }
552
553         /**
554          * Returns the SQL condition string built from the provided condition array
555          *
556          * This function operates with two modes.
557          * - Supplied with a field/value associative array, it builds simple strict
558          *   equality conditions linked by AND.
559          * - Supplied with a flat list, the first element is the condition string and
560          *   the following arguments are the values to be interpolated
561          *
562          * $condition = ["uid" => 1, "network" => 'dspr'];
563          * or:
564          * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
565          *
566          * In either case, the provided array is left with the parameters only
567          *
568          * @param array $condition
569          * @return string
570          */
571         public static function buildCondition(array &$condition = [])
572         {
573                 $condition = self::collapseCondition($condition);
574                 
575                 $condition_string = '';
576                 if (count($condition) > 0) {
577                         $condition_string = " WHERE (" . array_shift($condition) . ")";
578                 }
579
580                 return $condition_string;
581         }
582
583         /**
584          * Collapse an associative array condition into a SQL string + parameters condition array.
585          *
586          * ['uid' => 1, 'network' => ['dspr', 'apub']]
587          *
588          * gets transformed into
589          *
590          * ["`uid` = ? AND `network` IN (?, ?)", 1, 'dspr', 'apub']
591          *
592          * @param array $condition
593          * @return array
594          */
595         public static function collapseCondition(array $condition)
596         {
597                 // Ensures an always true condition is returned
598                 if (count($condition) < 1) {
599                         return ['1'];
600                 }
601
602                 reset($condition);
603                 $first_key = key($condition);
604
605                 if (is_int($first_key)) {
606                         // Already collapsed
607                         return $condition;
608                 }
609
610                 $values = [];
611                 $condition_string = "";
612                 foreach ($condition as $field => $value) {
613                         if ($condition_string != "") {
614                                 $condition_string .= " AND ";
615                         }
616
617                         if (is_array($value)) {
618                                 if (count($value)) {
619                                         /* Workaround for MySQL Bug #64791.
620                                          * Never mix data types inside any IN() condition.
621                                          * In case of mixed types, cast all as string.
622                                          * Logic needs to be consistent with DBA::p() data types.
623                                          */
624                                         $is_int = false;
625                                         $is_alpha = false;
626                                         foreach ($value as $single_value) {
627                                                 if (is_int($single_value)) {
628                                                         $is_int = true;
629                                                 } else {
630                                                         $is_alpha = true;
631                                                 }
632                                         }
633
634                                         if ($is_int && $is_alpha) {
635                                                 foreach ($value as &$ref) {
636                                                         if (is_int($ref)) {
637                                                                 $ref = (string)$ref;
638                                                         }
639                                                 }
640                                                 unset($ref); //Prevent accidental re-use.
641                                         }
642
643                                         $values = array_merge($values, array_values($value));
644                                         $placeholders = substr(str_repeat("?, ", count($value)), 0, -2);
645                                         $condition_string .= self::quoteIdentifier($field) . " IN (" . $placeholders . ")";
646                                 } else {
647                                         // Empty value array isn't supported by IN and is logically equivalent to no match
648                                         $condition_string .= "FALSE";
649                                 }
650                         } elseif (is_null($value)) {
651                                 $condition_string .= self::quoteIdentifier($field) . " IS NULL";
652                         } else {
653                                 $values[$field] = $value;
654                                 $condition_string .= self::quoteIdentifier($field) . " = ?";
655                         }
656                 }
657
658                 $condition = array_merge([$condition_string], array_values($values));
659
660                 return $condition;
661         }
662
663         /**
664          * Merges the provided conditions into a single collapsed one
665          *
666          * @param array ...$conditions One or more condition arrays
667          * @return array A collapsed condition
668          * @see DBA::collapseCondition() for the condition array formats
669          */
670         public static function mergeConditions(array ...$conditions)
671         {
672                 if (count($conditions) == 1) {
673                         return current($conditions);
674                 }
675
676                 $conditionStrings = [];
677                 $result = [];
678
679                 foreach ($conditions as $key => $condition) {
680                         if (!$condition) {
681                                 continue;
682                         }
683
684                         $condition = self::collapseCondition($condition);
685
686                         $conditionStrings[] = array_shift($condition);
687                         // The result array holds the eventual parameter values
688                         $result = array_merge($result, $condition);
689                 }
690
691                 if (count($conditionStrings)) {
692                         // We prepend the condition string at the end to form a collapsed condition array again
693                         array_unshift($result, implode(' AND ', $conditionStrings));
694                 }
695
696                 return $result;
697         }
698
699         /**
700          * Returns the SQL parameter string built from the provided parameter array
701          *
702          * Expected format for each key:
703          *
704          * group_by:
705          *  - list of column names
706          *
707          * order:
708          *  - numeric keyed column name => ASC
709          *  - associative element with boolean value => DESC (true), ASC (false)
710          *  - associative element with string value => 'ASC' or 'DESC' literally
711          *
712          * limit:
713          *  - single numeric value => count
714          *  - list with two numeric values => offset, count
715          *
716          * @param array $params
717          * @return string
718          */
719         public static function buildParameter(array $params = [])
720         {
721                 $groupby_string = '';
722                 if (!empty($params['group_by'])) {
723                         $groupby_string = " GROUP BY " . implode(', ', array_map(['self', 'quoteIdentifier'], $params['group_by']));
724                 }
725
726                 $order_string = '';
727                 if (isset($params['order'])) {
728                         $order_string = " ORDER BY ";
729                         foreach ($params['order'] AS $fields => $order) {
730                                 if ($order === 'RAND()') {
731                                         $order_string .= "RAND(), ";
732                                 } elseif (!is_int($fields)) {
733                                         if ($order !== 'DESC' && $order !== 'ASC') {
734                                                 $order = $order ? 'DESC' : 'ASC';
735                                         }
736
737                                         $order_string .= self::quoteIdentifier($fields) . " " . $order . ", ";
738                                 } else {
739                                         $order_string .= self::quoteIdentifier($order) . ", ";
740                                 }
741                         }
742                         $order_string = substr($order_string, 0, -2);
743                 }
744
745                 $limit_string = '';
746                 if (isset($params['limit']) && is_numeric($params['limit'])) {
747                         $limit_string = " LIMIT " . intval($params['limit']);
748                 }
749
750                 if (isset($params['limit']) && is_array($params['limit'])) {
751                         $limit_string = " LIMIT " . intval($params['limit'][0]) . ", " . intval($params['limit'][1]);
752                 }
753
754                 return $groupby_string . $order_string . $limit_string;
755         }
756
757         /**
758          * Fills an array with data from a query
759          *
760          * @param object $stmt statement object
761          * @param bool   $do_close
762          * @return array Data array
763          */
764         public static function toArray($stmt, $do_close = true)
765         {
766                 return DI::dba()->toArray($stmt, $do_close);
767         }
768
769         /**
770          * Returns the error number of the last query
771          *
772          * @return string Error number (0 if no error)
773          */
774         public static function errorNo()
775         {
776                 return DI::dba()->errorNo();
777         }
778
779         /**
780          * Returns the error message of the last query
781          *
782          * @return string Error message ('' if no error)
783          */
784         public static function errorMessage()
785         {
786                 return DI::dba()->errorMessage();
787         }
788
789         /**
790          * Closes the current statement
791          *
792          * @param object $stmt statement object
793          * @return boolean was the close successful?
794          */
795         public static function close($stmt)
796         {
797                 return DI::dba()->close($stmt);
798         }
799
800         /**
801          * Return a list of database processes
802          *
803          * @return array
804          *      'list' => List of processes, separated in their different states
805          *      'amount' => Number of concurrent database processes
806          * @throws \Exception
807          */
808         public static function processlist()
809         {
810                 return DI::dba()->processlist();
811         }
812
813         /**
814          * Fetch a database variable
815          *
816          * @param string $name
817          * @return string content
818          */
819         public static function getVariable(string $name)
820         {
821                 return DI::dba()->getVariable($name);
822         }
823
824         /**
825          * Checks if $array is a filled array with at least one entry.
826          *
827          * @param mixed $array A filled array with at least one entry
828          *
829          * @return boolean Whether $array is a filled array or an object with rows
830          */
831         public static function isResult($array)
832         {
833                 return DI::dba()->isResult($array);
834         }
835
836         /**
837          * Escapes a whole array
838          *
839          * @param mixed   $arr           Array with values to be escaped
840          * @param boolean $add_quotation add quotation marks for string values
841          * @return void
842          */
843         public static function escapeArray(&$arr, $add_quotation = false)
844         {
845                 DI::dba()->escapeArray($arr, $add_quotation);
846         }
847 }