Issues (2090)

public/main/inc/lib/database.lib.php (3 issues)

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Doctrine\Common\EventManager;
6
use Doctrine\DBAL\Connection;
7
use Doctrine\DBAL\DriverManager;
8
use Doctrine\DBAL\Types\Type;
9
use Doctrine\ORM\Configuration;
10
use Doctrine\ORM\EntityManager;
11
use Doctrine\ORM\ORMSetup;
12
use Symfony\Bridge\Doctrine\Types\UuidType;
13
14
class Database
15
{
16
    private static EntityManager $em;
17
    private static Connection $connection;
18
19
    /**
20
     * Setup doctrine only for the installation.
21
     *
22
     * @throws \Doctrine\ORM\ORMException
23
     * @throws \Doctrine\DBAL\Exception
24
     */
25
    public static function connect(array $params = [], string $entityRootPath = ''): void
26
    {
27
        $config = self::getDoctrineConfig($entityRootPath);
28
        $config->setAutoGenerateProxyClasses(true);
29
        $config->setEntityNamespaces(
30
            [
31
                'ChamiloCoreBundle' => 'Chamilo\CoreBundle\Entity',
32
                'ChamiloCourseBundle' => 'Chamilo\CourseBundle\Entity',
33
            ]
34
        );
35
36
        $params['charset'] = 'utf8';
37
38
        $evm = new EventManager();
39
        $timestampableListener = new Gedmo\Timestampable\TimestampableListener();
40
        $evm->addEventSubscriber($timestampableListener);
41
42
        if (false === Type::hasType('uuid')) {
43
            Type::addType('uuid', UuidType::class);
44
        }
45
46
        $connection = DriverManager::getConnection($params, $config, $evm);
47
48
        $entityManager = new EntityManager($connection, $config, $evm);
49
50
        self::setConnection($connection);
51
        self::setManager($entityManager);
52
    }
53
54
    public static function setManager(EntityManager $em)
55
    {
56
        self::$em = $em;
57
    }
58
59
    public static function setConnection(Connection $connection)
60
    {
61
        self::$connection = $connection;
62
    }
63
64
    public static function getConnection(): Connection
65
    {
66
        return self::$connection;
67
    }
68
69
    public static function getManager(): EntityManager
70
    {
71
        return self::$em;
72
    }
73
74
    /**
75
     * Returns the name of the main database.
76
     *
77
     * @throws \Doctrine\DBAL\Exception
78
     */
79
    public static function get_main_database(): bool|string|null
80
    {
81
        return self::getManager()->getConnection()->getDatabase();
82
    }
83
84
    /**
85
     * Get main table.
86
     */
87
    public static function get_main_table(string $table): string
88
    {
89
        return $table;
90
    }
91
92
    /**
93
     * Get course table.
94
     */
95
    public static function get_course_table(string $table): string
96
    {
97
        return DB_COURSE_PREFIX.$table;
98
    }
99
100
    /**
101
     * Counts the number of rows in a table.
102
     *
103
     * @throws Exception
104
     * @throws \Doctrine\DBAL\Exception
105
     *
106
     * @deprecated
107
     */
108
    public static function count_rows(string $table): int
109
    {
110
        $result = self::query("SELECT COUNT(*) AS n FROM $table");
111
112
        return (int) $result->fetchOne();
113
    }
114
115
    /**
116
     * Returns the number of affected rows in the last database operation.
117
     *
118
     * @throws \Doctrine\DBAL\Exception
119
     */
120
    public static function affected_rows(\Doctrine\DBAL\Result $result): int
121
    {
122
        return $result->rowCount();
123
    }
124
125
    /**
126
     * Escapes a string to insert into the database as text.
127
     */
128
    public static function escape_string(mixed $string): string
129
    {
130
        $string = self::getManager()->getConnection()->quote($string);
131
        // The quote method from PDO also adds quotes around the string, which
132
        // is not how the legacy mysql_real_escape_string() was used in
133
        // Chamilo, so we need to remove the quotes around. Using trim will
134
        // remove more than one quote if they are sequenced, generating
135
        // broken queries and SQL injection risks
136
        return substr($string, 1, -1);
137
    }
138
139
    /**
140
     * Gets the (associative) array from a SQL result (as returned by Database::query).
141
     *
142
     * @throws \Doctrine\DBAL\Exception
143
     */
144
    public static function fetch_array(\Doctrine\DBAL\Result $result): mixed
145
    {
146
        $data = $result->fetchAssociative();
147
148
        if (empty($data)) {
149
            return [];
150
        }
151
152
        $return = $data;
153
154
        foreach ($data as $value) {
155
            $return[] = $value;
156
        }
157
158
        return $return;
159
    }
160
161
    /**
162
     * Gets an associative array from a SQL result (as returned by Database::query).
163
     *
164
     * @throws \Doctrine\DBAL\Exception
165
     */
166
    public static function fetch_assoc(\Doctrine\DBAL\Result $result): array|bool
167
    {
168
        return $result->fetchAssociative();
169
    }
170
171
    /**
172
     * Gets the next row of the result of the SQL query
173
     * (as returned by Database::query) in an object form.
174
     *
175
     * @throws \Doctrine\DBAL\Exception
176
     */
177
    public static function fetch_object(\Doctrine\DBAL\Result $result): ?stdClass
178
    {
179
        $data = $result->fetchAssociative();
180
181
        if (empty($data)) {
182
            return null;
183
        }
184
185
        $object = new stdClass();
186
187
        foreach ($data as $key => $value) {
188
            $object->$key = $value;
189
        }
190
191
        return $object;
192
    }
193
194
    /**
195
     * Gets the array from a SQL result (as returned by Database::query)
196
     * help to achieve database independence.
197
     *
198
     * @throws \Doctrine\DBAL\Exception
199
     */
200
    public static function fetch_row(\Doctrine\DBAL\Result $result): array
201
    {
202
        $row = $result->fetchNumeric();
203
204
        return empty($row) ? [] : $row;
205
    }
206
207
    /**
208
     * Gets the ID of the last item inserted into the database.
209
     *
210
     * @throws \Doctrine\DBAL\Exception
211
     */
212
    public static function insert_id(): int
213
    {
214
        return (int) self::getManager()->getConnection()->lastInsertId();
215
    }
216
217
    /**
218
     * @throws \Doctrine\DBAL\Exception
219
     */
220
    public static function num_rows(\Doctrine\DBAL\Result $result): int
221
    {
222
        return $result->rowCount();
223
    }
224
225
    /**
226
     * Acts as the relative *_result() function of most DB drivers and fetches a
227
     * specific line and a field.
228
     *
229
     * @throws \Doctrine\DBAL\Exception
230
     */
231
    public static function result(\Doctrine\DBAL\Result $resource, int $row, string $field): mixed
232
    {
233
        if ($resource->rowCount() > 0) {
234
            $result = $resource->fetchAllAssociative();
235
236
            $resultRow = $result[$row];
237
238
            foreach ($result[$row] as $value) {
239
                $resultRow[] = $value;
240
            }
241
242
            return $resultRow[$field];
243
        }
244
245
        return null;
246
    }
247
248
    /**
249
     * Wrapper to executes a query on the defined database handler.
250
     * @throws Exception
251
     */
252
    public static function query(string $query): ?\Doctrine\DBAL\Result
253
    {
254
        $connection = self::getManager()->getConnection();
255
        $result = null;
256
        try {
257
            $result = $connection->executeQuery($query);
258
        } catch (Exception $e) {
259
            self::handleError($e);
260
        }
261
262
        return $result;
263
    }
264
265
    /**
266
     * @throws Exception
267
     */
268
    public static function handleError(Exception $e)
269
    {
270
        $debug = 'test' === api_get_setting('server_type');
271
        if ($debug) {
272
            throw $e;
273
        } else {
274
            error_log($e->getMessage());
275
            api_not_allowed(false, get_lang('There happened an unknown error. Please contact the platform administrator.'));
276
        }
277
    }
278
279
    /**
280
     * Stores a query result into an array.
281
     *
282
     * @throws \Doctrine\DBAL\Exception
283
     */
284
    public static function store_result(\Doctrine\DBAL\Result $result, $option = 'BOTH'): array
285
    {
286
        if ('NUM' === $option) {
287
            return $result->fetchAllNumeric();
288
        }
289
290
        return $result->fetchAllAssociative();
291
    }
292
293
    /**
294
     * Database insert.
295
     *
296
     * @throws \Doctrine\DBAL\Exception
297
     * @throws Exception
298
     */
299
    public static function insert(string $table_name, array $attributes, bool $show_query = false, array $options = []): bool|int
300
    {
301
        if (empty($attributes) || empty($table_name)) {
302
            return false;
303
        }
304
305
        $params = array_keys($attributes);
306
307
        if (!empty($params)) {
308
            $prefix = (!empty($options['ignore'])) ? 'INSERT IGNORE INTO' : 'INSERT INTO';
309
            $sql = $prefix.' '.$table_name.' ('.implode(',', $params).')
310
                VALUES (:'.implode(', :', $params).')';
311
312
            if ($show_query) {
313
                var_dump($sql);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($sql) looks like debug code. Are you sure you do not want to remove it?
Loading history...
314
                error_log($sql);
315
            }
316
317
            try {
318
                self::getConnection()
319
                    ->prepare($sql)
320
                    ->executeQuery($attributes)
321
                ;
322
            } catch (Exception $e) {
323
                self::handleError($e);
324
325
                return false;
326
            }
327
328
            return (int) self::getManager()->getConnection()->lastInsertId();
329
        }
330
331
        return false;
332
    }
333
334
    /**
335
     * Wrapper executing an SQL update query based on the given attributes array
336
     * @param string $tableName       use Database::get_main_table
337
     * @param array  $attributes      Values to updates
338
     *                                Example: $params['name'] = 'Julio'; $params['lastname'] = 'Montoya';
339
     * @param array  $whereConditions where conditions i.e. array('id = ?' =>'4')
340
     * @param bool   $showQuery
341
     *
342
     * @throws Exception
343
     *
344
     * @return bool|int
345
     */
346
    public static function update(
347
        string $tableName,
348
        array $attributes,
349
        array $whereConditions = [],
350
        bool $showQuery = false
351
    ): bool|int {
352
        if (!empty($tableName) && !empty($attributes)) {
353
            $updateSql = '';
354
            $count = 1;
355
356
            foreach ($attributes as $key => $value) {
357
                if ($showQuery) {
358
                    echo $key.': '.$value.PHP_EOL;
359
                }
360
                $updateSql .= "$key = :$key ";
361
                if ($count < count($attributes)) {
362
                    $updateSql .= ', ';
363
                }
364
                $count++;
365
            }
366
367
            if (!empty($updateSql)) {
368
                // Parsing and cleaning the where conditions
369
                $whereReturn = self::parse_where_conditions($whereConditions);
370
                $sql = "UPDATE $tableName SET $updateSql $whereReturn ";
371
372
                try {
373
                    $statement = self::getManager()->getConnection()->prepare($sql);
374
                    $result = $statement->executeQuery($attributes);
375
                } catch (Exception $e) {
376
                    self::handleError($e);
377
378
                    return false;
379
                }
380
381
                if ($showQuery) {
382
                    var_dump($sql);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($sql) looks like debug code. Are you sure you do not want to remove it?
Loading history...
383
                    var_dump($attributes);
384
                    var_dump($whereConditions);
385
                }
386
387
                return $result->rowCount();
388
            }
389
        }
390
391
        return false;
392
    }
393
394
    /**
395
     * Experimental useful database finder.
396
     *
397
     * @throws Exception
398
     *
399
     * @todo    lot of stuff to do here
400
     * @todo    known issues, it doesn't work when using LIKE conditions
401
     *
402
     * @example ['where'=> ['course_code LIKE "?%"']]
403
     * @example ['where'=> ['type = ? AND category = ?' => ['setting', 'Plugins']]]
404
     * @example ['where'=> ['name = "Julio" AND lastname = "montoya"']]
405
     */
406
    public static function select(
407
        string|array $columns,
408
        string $table_name,
409
        array $conditions = [],
410
        string $type_result = 'all',
411
        string $option = 'ASSOC',
412
        bool $debug = false
413
    ): int|array {
414
        if ('count' === $type_result) {
415
            $conditions['LIMIT'] = null;
416
            $conditions['limit'] = null;
417
        }
418
        $conditions = self::parse_conditions($conditions);
419
420
        //@todo we could do a describe here to check the columns ...
421
        if (is_array($columns)) {
422
            $clean_columns = implode(',', $columns);
423
        } else {
424
            if ('*' === $columns) {
425
                $clean_columns = '*';
426
            } else {
427
                $clean_columns = (string) $columns;
428
            }
429
        }
430
431
        if ('count' === $type_result) {
432
            $clean_columns = ' count(*) count ';
433
        }
434
        $sql = "SELECT $clean_columns FROM $table_name $conditions";
435
        if ($debug) {
436
            var_dump($sql);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($sql) looks like debug code. Are you sure you do not want to remove it?
Loading history...
437
        }
438
        $result = self::query($sql);
439
        if ('count' === $type_result) {
440
            $row = self::fetch_array($result);
441
            if ($row) {
442
                return (int) $row['count'];
443
            }
444
445
            return 0;
446
        }
447
        $array = [];
448
449
        if ('all' === $type_result) {
450
            while ($row = self::fetch_array($result)) {
451
                if (isset($row['id'])) {
452
                    $array[$row['id']] = $row;
453
                } else {
454
                    $array[] = $row;
455
                }
456
            }
457
        } else {
458
            $array = self::fetch_array($result);
459
        }
460
461
        return $array;
462
    }
463
464
    /**
465
     * Parses WHERE/ORDER conditions i.e. array('where'=>array('id = ?' =>'4'), 'order'=>'id DESC').
466
     *
467
     * @todo known issues, it doesn't work when using
468
     * LIKE conditions example: array('where'=>array('course_code LIKE "?%"'))
469
     */
470
    public static function parse_conditions(array $conditions): string
471
    {
472
        if (empty($conditions)) {
473
            return '';
474
        }
475
        $return_value = $where_return = '';
476
        foreach ($conditions as $type_condition => $condition_data) {
477
            if (false == $condition_data) {
478
                continue;
479
            }
480
            $type_condition = strtolower($type_condition);
481
            switch ($type_condition) {
482
                case 'where':
483
                    foreach ($condition_data as $condition => $value_array) {
484
                        if (is_array($value_array)) {
485
                            $clean_values = [];
486
                            foreach ($value_array as $item) {
487
                                $item = self::escape_string($item);
488
                                $clean_values[] = $item;
489
                            }
490
                        } else {
491
                            $value_array = self::escape_string($value_array);
492
                            $clean_values = [$value_array];
493
                        }
494
495
                        if (!empty($condition) && '' != $clean_values) {
496
                            $condition = str_replace('%', "'@percentage@'", $condition); //replace "%"
497
                            $condition = str_replace("'?'", "%s", $condition);
498
                            $condition = str_replace("?", "%s", $condition);
499
500
                            $condition = str_replace("@%s@", "@-@", $condition);
501
                            $condition = str_replace("%s", "'%s'", $condition);
502
                            $condition = str_replace("@-@", "@%s@", $condition);
503
504
                            // Treat conditions as string
505
                            $condition = vsprintf($condition, $clean_values);
506
                            $condition = str_replace('@percentage@', '%', $condition); //replace "%"
507
                            $where_return .= $condition;
508
                        }
509
                    }
510
511
                    if (!empty($where_return)) {
512
                        $return_value = " WHERE $where_return";
513
                    }
514
                    break;
515
                case 'order':
516
                    $order_array = $condition_data;
517
518
                    if (!empty($order_array)) {
519
                        // 'order' => 'id desc, name desc'
520
                        $order_array = self::escape_string($order_array);
521
                        $new_order_array = explode(',', $order_array);
522
                        $temp_value = [];
523
524
                        foreach ($new_order_array as $element) {
525
                            $element = explode(' ', $element);
526
                            $element = array_filter($element);
527
                            $element = array_values($element);
528
529
                            if (!empty($element[1])) {
530
                                $element[1] = strtolower($element[1]);
531
                                $order = 'DESC';
532
                                if (in_array($element[1], ['desc', 'asc'])) {
533
                                    $order = $element[1];
534
                                }
535
                                $temp_value[] = ' `'.$element[0].'` '.$order.' ';
536
                            } else {
537
                                //by default DESC
538
                                $temp_value[] = ' `'.$element[0].'` DESC ';
539
                            }
540
                        }
541
                        if (!empty($temp_value)) {
542
                            $return_value .= ' ORDER BY '.implode(', ', $temp_value);
543
                        }
544
                    }
545
                    break;
546
                case 'limit':
547
                    $limit_array = explode(',', $condition_data);
548
                    if (!empty($limit_array)) {
549
                        if (count($limit_array) > 1) {
550
                            $return_value .= ' LIMIT '.intval($limit_array[0]).' , '.intval($limit_array[1]);
551
                        } else {
552
                            $return_value .= ' LIMIT '.intval($limit_array[0]);
553
                        }
554
                    }
555
                    break;
556
            }
557
        }
558
559
        return $return_value;
560
    }
561
562
    public static function parse_where_conditions(array $conditions): string
563
    {
564
        return self::parse_conditions(['where' => $conditions]);
565
    }
566
567
    /**
568
     * @throws Exception
569
     */
570
    public static function delete(string $tableName, array $where_conditions, bool $show_query = false): int
571
    {
572
        $where_return = self::parse_where_conditions($where_conditions);
573
        $sql = "DELETE FROM $tableName $where_return ";
574
        if ($show_query) {
575
            echo $sql;
576
            echo '<br />';
577
        }
578
        $result = self::query($sql);
579
580
        return self::affected_rows($result);
581
    }
582
583
    /**
584
     * Get Doctrine configuration.
585
     */
586
    public static function getDoctrineConfig(string $path): Configuration
587
    {
588
        $cache = null;
589
        $path = !empty($path) ? $path : api_get_path(SYMFONY_SYS_PATH);
590
591
        $paths = [
592
            $path.'src/Chamilo/CoreBundle/Entity',
593
            $path.'src/Chamilo/CourseBundle/Entity',
594
        ];
595
596
        $proxyDir = $path.'var/cache/';
597
598
        return ORMSetup::createAttributeMetadataConfiguration(
599
            $paths,
600
            true,
601
            $proxyDir,
602
            $cache
603
        );
604
    }
605
606
    /**
607
     * @throws \Doctrine\DBAL\Exception
608
     */
609
    public static function tableExists(string $table): bool
610
    {
611
        return self::getManager()->getConnection()->createSchemaManager()->tablesExist($table);
612
    }
613
614
    /**
615
     * @throws \Doctrine\DBAL\Exception
616
     */
617
    public static function listTableColumns(string $table): array
618
    {
619
        return self::getManager()->getConnection()->createSchemaManager()->listTableColumns($table);
620
    }
621
622
    public static function escapeField($field): string
623
    {
624
        return self::escape_string(preg_replace("/[^a-zA-Z0-9_.]/", '', $field));
625
    }
626
}
627