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