]> git.mxchange.org Git - friendica.git/blob - src/Database/DBA.php
Issue 11508: Sanitizing date fields for events and polls
[friendica.git] / src / Database / DBA.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
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          *
392          * @return boolean was the delete successful?
393          * @throws \Exception
394          */
395         public static function delete($table, array $conditions, array $options = [])
396         {
397                 return DI::dba()->delete($table, $conditions, $options);
398         }
399
400         /**
401          * Updates rows in the database.
402          *
403          * When $old_fields is set to an array,
404          * the system will only do an update if the fields in that array changed.
405          *
406          * Attention:
407          * Only the values in $old_fields are compared.
408          * This is an intentional behaviour.
409          *
410          * Example:
411          * We include the timestamp field in $fields but not in $old_fields.
412          * Then the row will only get the new timestamp when the other fields had changed.
413          *
414          * When $old_fields is set to a boolean value the system will do this compare itself.
415          * When $old_fields is set to "true" the system will do an insert if the row doesn't exists.
416          *
417          * Attention:
418          * Only set $old_fields to a boolean value when you are sure that you will update a single row.
419          * When you set $old_fields to "true" then $fields must contain all relevant fields!
420          *
421          * @param string|array  $table      Table name or array [schema => table]
422          * @param array         $fields     contains the fields that are updated
423          * @param array         $condition  condition array with the key values
424          * @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate, false = don't update identical fields)
425          *
426          * @return boolean was the update successfull?
427          * @throws \Exception
428          */
429         public static function update($table, $fields, $condition, $old_fields = [])
430         {
431                 return DI::dba()->update($table, $fields, $condition, $old_fields);
432         }
433
434         /**
435          * Retrieve a single record from a table and returns it in an associative array
436          *
437          * @param string|array $table     Table name or array [schema => table]
438          * @param array        $fields
439          * @param array        $condition
440          * @param array        $params
441          * @return bool|array
442          * @throws \Exception
443          * @see   self::select
444          */
445         public static function selectFirst($table, array $fields = [], array $condition = [], $params = [])
446         {
447                 return DI::dba()->selectFirst($table, $fields, $condition, $params);
448         }
449
450         /**
451          * Select rows from a table and fills an array with the data
452          *
453          * @param string|array $table     Table name or array [schema => table]
454          * @param array        $fields    Array of selected fields, empty for all
455          * @param array        $condition Array of fields for condition
456          * @param array        $params    Array of several parameters
457          *
458          * @return array Data array
459          * @throws \Exception
460          * @see   self::select
461          */
462         public static function selectToArray($table, array $fields = [], array $condition = [], array $params = [])
463         {
464                 return DI::dba()->selectToArray($table, $fields, $condition, $params);
465         }
466
467         /**
468          * Select rows from a table
469          *
470          * @param string|array $table     Table name or array [schema => table]
471          * @param array        $fields    Array of selected fields, empty for all
472          * @param array        $condition Array of fields for condition
473          * @param array        $params    Array of several parameters
474          *
475          * @return boolean|object
476          *
477          * Example:
478          * $table = "post";
479          * $fields = array("id", "uri", "uid", "network");
480          *
481          * $condition = array("uid" => 1, "network" => 'dspr');
482          * or:
483          * $condition = array("`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr');
484          *
485          * $params = array("order" => array("id", "received" => true), "limit" => 10);
486          *
487          * $data = DBA::select($table, $fields, $condition, $params);
488          * @throws \Exception
489          */
490         public static function select($table, array $fields = [], array $condition = [], array $params = [])
491         {
492                 return DI::dba()->select($table, $fields, $condition, $params);
493         }
494
495         /**
496          * Counts the rows from a table satisfying the provided condition
497          *
498          * @param string|array $table     Table name or array [schema => table]
499          * @param array        $condition array of fields for condition
500          * @param array        $params    Array of several parameters
501          *
502          * @return int
503          *
504          * Example:
505          * $table = "post";
506          *
507          * $condition = ["uid" => 1, "network" => 'dspr'];
508          * or:
509          * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
510          *
511          * $count = DBA::count($table, $condition);
512          * @throws \Exception
513          */
514         public static function count($table, array $condition = [], array $params = [])
515         {
516                 return DI::dba()->count($table, $condition, $params);
517         }
518
519         /**
520          * Build the table query substring from one or more tables, with or without a schema.
521          *
522          * Expected formats:
523          * - table
524          * - [table1, table2, ...]
525          * - [schema1 => table1, schema2 => table2, table3, ...]
526          *
527          * @param string|array $tables
528          * @return string
529          */
530         public static function buildTableString($tables)
531         {
532                 if (is_string($tables)) {
533                         $tables = [$tables];
534                 }
535
536                 $quotedTables = [];
537
538                 foreach ($tables as $schema => $table) {
539                         if (is_numeric($schema)) {
540                                 $quotedTables[] = self::quoteIdentifier($table);
541                         } else {
542                                 $quotedTables[] = self::quoteIdentifier($schema) . '.' . self::quoteIdentifier($table);
543                         }
544                 }
545
546                 return implode(', ', $quotedTables);
547         }
548
549         /**
550          * Escape an identifier (table or field name)
551          *
552          * @param $identifier
553          * @return string
554          */
555         public static function quoteIdentifier($identifier)
556         {
557                 return '`' . str_replace('`', '``', $identifier) . '`';
558         }
559
560         /**
561          * Returns the SQL condition string built from the provided condition array
562          *
563          * This function operates with two modes.
564          * - Supplied with a field/value associative array, it builds simple strict
565          *   equality conditions linked by AND.
566          * - Supplied with a flat list, the first element is the condition string and
567          *   the following arguments are the values to be interpolated
568          *
569          * $condition = ["uid" => 1, "network" => 'dspr'];
570          * or:
571          * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
572          *
573          * In either case, the provided array is left with the parameters only
574          *
575          * @param array $condition
576          * @return string
577          */
578         public static function buildCondition(array &$condition = [])
579         {
580                 $condition = self::collapseCondition($condition);
581                 
582                 $condition_string = '';
583                 if (count($condition) > 0) {
584                         $condition_string = " WHERE (" . array_shift($condition) . ")";
585                 }
586
587                 return $condition_string;
588         }
589
590         /**
591          * Collapse an associative array condition into a SQL string + parameters condition array.
592          *
593          * ['uid' => 1, 'network' => ['dspr', 'apub']]
594          *
595          * gets transformed into
596          *
597          * ["`uid` = ? AND `network` IN (?, ?)", 1, 'dspr', 'apub']
598          *
599          * @param array $condition
600          * @return array
601          */
602         public static function collapseCondition(array $condition)
603         {
604                 // Ensures an always true condition is returned
605                 if (count($condition) < 1) {
606                         return ['1'];
607                 }
608
609                 reset($condition);
610                 $first_key = key($condition);
611
612                 if (is_int($first_key)) {
613                         // Already collapsed
614                         return $condition;
615                 }
616
617                 $values = [];
618                 $condition_string = "";
619                 foreach ($condition as $field => $value) {
620                         if ($condition_string != "") {
621                                 $condition_string .= " AND ";
622                         }
623
624                         if (is_array($value)) {
625                                 if (count($value)) {
626                                         /* Workaround for MySQL Bug #64791.
627                                          * Never mix data types inside any IN() condition.
628                                          * In case of mixed types, cast all as string.
629                                          * Logic needs to be consistent with DBA::p() data types.
630                                          */
631                                         $is_int = false;
632                                         $is_alpha = false;
633                                         foreach ($value as $single_value) {
634                                                 if (is_int($single_value)) {
635                                                         $is_int = true;
636                                                 } else {
637                                                         $is_alpha = true;
638                                                 }
639                                         }
640
641                                         if ($is_int && $is_alpha) {
642                                                 foreach ($value as &$ref) {
643                                                         if (is_int($ref)) {
644                                                                 $ref = (string)$ref;
645                                                         }
646                                                 }
647                                                 unset($ref); //Prevent accidental re-use.
648                                         }
649
650                                         $values = array_merge($values, array_values($value));
651                                         $placeholders = substr(str_repeat("?, ", count($value)), 0, -2);
652                                         $condition_string .= self::quoteIdentifier($field) . " IN (" . $placeholders . ")";
653                                 } else {
654                                         // Empty value array isn't supported by IN and is logically equivalent to no match
655                                         $condition_string .= "FALSE";
656                                 }
657                         } elseif (is_null($value)) {
658                                 $condition_string .= self::quoteIdentifier($field) . " IS NULL";
659                         } else {
660                                 $values[$field] = $value;
661                                 $condition_string .= self::quoteIdentifier($field) . " = ?";
662                         }
663                 }
664
665                 $condition = array_merge([$condition_string], array_values($values));
666
667                 return $condition;
668         }
669
670         /**
671          * Merges the provided conditions into a single collapsed one
672          *
673          * @param array ...$conditions One or more condition arrays
674          * @return array A collapsed condition
675          * @see DBA::collapseCondition() for the condition array formats
676          */
677         public static function mergeConditions(array ...$conditions)
678         {
679                 if (count($conditions) == 1) {
680                         return current($conditions);
681                 }
682
683                 $conditionStrings = [];
684                 $result = [];
685
686                 foreach ($conditions as $key => $condition) {
687                         if (!$condition) {
688                                 continue;
689                         }
690
691                         $condition = self::collapseCondition($condition);
692
693                         $conditionStrings[] = array_shift($condition);
694                         // The result array holds the eventual parameter values
695                         $result = array_merge($result, $condition);
696                 }
697
698                 if (count($conditionStrings)) {
699                         // We prepend the condition string at the end to form a collapsed condition array again
700                         array_unshift($result, implode(' AND ', $conditionStrings));
701                 }
702
703                 return $result;
704         }
705
706         /**
707          * Returns the SQL parameter string built from the provided parameter array
708          *
709          * Expected format for each key:
710          *
711          * group_by:
712          *  - list of column names
713          *
714          * order:
715          *  - numeric keyed column name => ASC
716          *  - associative element with boolean value => DESC (true), ASC (false)
717          *  - associative element with string value => 'ASC' or 'DESC' literally
718          *
719          * limit:
720          *  - single numeric value => count
721          *  - list with two numeric values => offset, count
722          *
723          * @param array $params
724          * @return string
725          */
726         public static function buildParameter(array $params = [])
727         {
728                 $groupby_string = '';
729                 if (!empty($params['group_by'])) {
730                         $groupby_string = " GROUP BY " . implode(', ', array_map(['self', 'quoteIdentifier'], $params['group_by']));
731                 }
732
733                 $order_string = '';
734                 if (isset($params['order'])) {
735                         $order_string = " ORDER BY ";
736                         foreach ($params['order'] as $fields => $order) {
737                                 if ($order === 'RAND()') {
738                                         $order_string .= "RAND(), ";
739                                 } elseif (!is_int($fields)) {
740                                         if ($order !== 'DESC' && $order !== 'ASC') {
741                                                 $order = $order ? 'DESC' : 'ASC';
742                                         }
743
744                                         $order_string .= self::quoteIdentifier($fields) . " " . $order . ", ";
745                                 } else {
746                                         $order_string .= self::quoteIdentifier($order) . ", ";
747                                 }
748                         }
749                         $order_string = substr($order_string, 0, -2);
750                 }
751
752                 $limit_string = '';
753                 if (isset($params['limit']) && is_numeric($params['limit'])) {
754                         $limit_string = " LIMIT " . intval($params['limit']);
755                 }
756
757                 if (isset($params['limit']) && is_array($params['limit'])) {
758                         $limit_string = " LIMIT " . intval($params['limit'][0]) . ", " . intval($params['limit'][1]);
759                 }
760
761                 return $groupby_string . $order_string . $limit_string;
762         }
763
764         /**
765          * Fills an array with data from a query
766          *
767          * @param object $stmt     statement object
768          * @param bool   $do_close Close database connection after last row
769          * @param int    $count    maximum number of rows to be fetched
770          *
771          * @return array Data array
772          */
773         public static function toArray($stmt, $do_close = true, int $count = 0)
774         {
775                 return DI::dba()->toArray($stmt, $do_close, $count);
776         }
777
778         /**
779          * Cast field types according to the table definition
780          *
781          * @param string $table
782          * @param array  $fields
783          * @return array casted fields
784          */
785         public static function castFields(string $table, array $fields)
786         {
787                 return DI::dba()->castFields($table, $fields);
788         }
789
790         /**
791          * Returns the error number of the last query
792          *
793          * @return string Error number (0 if no error)
794          */
795         public static function errorNo()
796         {
797                 return DI::dba()->errorNo();
798         }
799
800         /**
801          * Returns the error message of the last query
802          *
803          * @return string Error message ('' if no error)
804          */
805         public static function errorMessage()
806         {
807                 return DI::dba()->errorMessage();
808         }
809
810         /**
811          * Closes the current statement
812          *
813          * @param object $stmt statement object
814          * @return boolean was the close successful?
815          */
816         public static function close($stmt)
817         {
818                 return DI::dba()->close($stmt);
819         }
820
821         /**
822          * Return a list of database processes
823          *
824          * @return array
825          *      'list' => List of processes, separated in their different states
826          *      'amount' => Number of concurrent database processes
827          * @throws \Exception
828          */
829         public static function processlist()
830         {
831                 return DI::dba()->processlist();
832         }
833
834         /**
835          * Fetch a database variable
836          *
837          * @param string $name
838          * @return string content
839          */
840         public static function getVariable(string $name)
841         {
842                 return DI::dba()->getVariable($name);
843         }
844
845         /**
846          * Checks if $array is a filled array with at least one entry.
847          *
848          * @param mixed $array A filled array with at least one entry
849          *
850          * @return boolean Whether $array is a filled array or an object with rows
851          */
852         public static function isResult($array)
853         {
854                 return DI::dba()->isResult($array);
855         }
856
857         /**
858          * Escapes a whole array
859          *
860          * @param mixed   $arr           Array with values to be escaped
861          * @param boolean $add_quotation add quotation marks for string values
862          * @return void
863          */
864         public static function escapeArray(&$arr, $add_quotation = false)
865         {
866                 DI::dba()->escapeArray($arr, $add_quotation);
867         }
868 }