Issues (1870)

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

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