Issues (1868)

public/main/install/install.lib.php (1 issue)

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\DataFixtures\LanguageFixtures;
6
use Chamilo\CoreBundle\Entity\AccessUrl;
7
use Chamilo\CoreBundle\Entity\User;
8
use Chamilo\CoreBundle\Entity\UserAuthSource;
9
use Chamilo\CoreBundle\Framework\Container;
10
use Chamilo\CoreBundle\Repository\GroupRepository;
11
use Chamilo\CoreBundle\Repository\Node\AccessUrlRepository;
12
use Chamilo\CoreBundle\Tool\ToolChain;
13
use Doctrine\DBAL\Connection;
14
use Doctrine\Migrations\Configuration\Connection\ExistingConnection;
15
use Doctrine\Migrations\Configuration\Migration\PhpFile;
16
use Doctrine\Migrations\DependencyFactory;
17
use Doctrine\Migrations\Query\Query;
18
use Doctrine\ORM\EntityManager;
19
use Symfony\Component\Console\Output\NullOutput;
20
use Symfony\Component\DependencyInjection\Container as SymfonyContainer;
21
use Symfony\Component\Dotenv\Dotenv;
22
use Symfony\Component\Console\Input\ArrayInput;
23
use Symfony\Component\Console\Output\BufferedOutput;
24
use Symfony\Bundle\FrameworkBundle\Console\Application;
25
26
/*
27
 * Chamilo LMS
28
 * This file contains functions used by the install and upgrade scripts.
29
 *
30
 * Ideas for future additions:
31
 * - a function get_old_version_settings to retrieve the config file settings
32
 *   of older versions before upgrading.
33
 */
34
define('SYSTEM_CONFIG_FILENAME', 'configuration.dist.php');
35
36
/**
37
 * This function detects whether the system has been already installed.
38
 * It should be used for prevention from second running the installation
39
 * script and as a result - destroying a production system.
40
 *
41
 * @return bool The detected result;
42
 *
43
 * @author Ivan Tcholakov, 2010;
44
 */
45
function isAlreadyInstalledSystem()
46
{
47
    global $new_version, $_configuration;
48
49
    if (empty($new_version)) {
50
        return true; // Must be initialized.
51
    }
52
53
    $current_config_file = api_get_path(CONFIGURATION_PATH).'configuration.php';
54
    if (!file_exists($current_config_file)) {
55
        return false; // Configuration file does not exist, install the system.
56
    }
57
    require $current_config_file;
58
59
    $current_version = null;
60
    if (isset($_configuration['system_version'])) {
61
        $current_version = trim($_configuration['system_version']);
62
    }
63
64
    // If the current version is old, upgrading is assumed, the installer goes ahead.
65
    return empty($current_version) ? false : version_compare($current_version, $new_version, '>=');
66
}
67
68
/**
69
 * This function checks if a php extension exists or not and returns an HTML status string.
70
 *
71
 * @param string $extensionName Name of the PHP extension to be checked
72
 * @param string $returnSuccess Text to show when extension is available (defaults to 'Yes')
73
 * @param string $returnFailure Text to show when extension is available (defaults to 'No')
74
 * @param bool   $optional      Whether this extension is optional (then show unavailable text in orange rather than
75
 *                              red)
76
 * @param string $enabledTerm   If this string is not null, then use to check if the corresponding parameter is = 1.
77
 *                              If not, mention it's present but not enabled. For example, for opcache, this should be
78
 *                              'opcache.enable'
79
 *
80
 * @return array
81
 *
82
 * @author  Christophe Gesch??
83
 * @author  Patrick Cool <[email protected]>, Ghent University
84
 * @author  Yannick Warnier <[email protected]>
85
 */
86
function checkExtension(
87
    string $extensionName,
88
    string $returnSuccess = 'Yes',
89
    string $returnFailure = 'No',
90
    bool $optional = false,
91
    string $enabledTerm = ''
92
): array {
93
    if (extension_loaded($extensionName)) {
94
        if (!empty($enabledTerm)) {
95
            $isEnabled = ini_get($enabledTerm);
96
            if ('1' == $isEnabled) {
97
                return [
98
                    'severity' => 'success',
99
                    'message' => $returnSuccess,
100
                ];
101
            } else {
102
                if ($optional) {
103
                    return [
104
                        'severity' => 'warning',
105
                        'message' => get_lang('Extension installed but not enabled'),
106
                    ];
107
                }
108
109
                return [
110
                    'severity' => 'danger',
111
                    'message' => get_lang('Extension installed but not enabled'),
112
                ];
113
            }
114
        }
115
116
        return [
117
            'severity' => 'success',
118
            'message' => $returnSuccess,
119
        ];
120
    } else {
121
        if ($optional) {
122
            return [
123
                'severity' => 'warning',
124
                'message' => $returnFailure,
125
            ];
126
        }
127
128
        return [
129
            'severity' => 'danger',
130
            'message' => $returnFailure,
131
        ];
132
    }
133
}
134
135
/**
136
 * This function checks whether a php setting matches the recommended value.
137
 *
138
 * @param string $phpSetting       A PHP setting to check
139
 * @param string $recommendedValue A recommended value to show on screen
140
 *
141
 * @author Patrick Cool <[email protected]>, Ghent University
142
 */
143
function checkPhpSetting(
144
    string $phpSetting,
145
    string $recommendedValue
146
): array {
147
    $currentPhpValue = getPhpSetting($phpSetting);
148
149
    return $currentPhpValue == $recommendedValue
150
        ? ['severity' => 'success', 'value' => $currentPhpValue]
151
        : ['severity' => 'danger', 'value' => $currentPhpValue];
152
}
153
154
/**
155
 * This function return the value of a php.ini setting if not "" or if exists,
156
 * otherwise return false.
157
 *
158
 * @param string $phpSetting The name of a PHP setting
159
 *
160
 * @return mixed The value of the setting, or false if not found
161
 */
162
function checkPhpSettingExists($phpSetting)
163
{
164
    if ('' != ini_get($phpSetting)) {
165
        return ini_get($phpSetting);
166
    }
167
168
    return false;
169
}
170
171
/**
172
 * Returns a textual value ('ON' or 'OFF') based on a requester 2-state ini- configuration setting.
173
 *
174
 * @param string $val a php ini value
175
 *
176
 * @return bool ON or OFF
177
 *
178
 * @author Joomla <http://www.joomla.org>
179
 */
180
function getPhpSetting($val)
181
{
182
    $value = ini_get($val);
183
    switch ($val) {
184
        case 'display_errors':
185
            global $originalDisplayErrors;
186
            $value = $originalDisplayErrors;
187
            break;
188
    }
189
190
    return '1' == $value ? 'ON' : 'OFF';
191
}
192
193
/**
194
 * This function returns a string "true" or "false" according to the passed parameter.
195
 *
196
 * @param int $var The variable to present as text
197
 *
198
 * @return string the string "true" or "false"
199
 *
200
 * @author Christophe Gesch??
201
 */
202
function trueFalse($var)
203
{
204
    return $var ? 'true' : 'false';
205
}
206
207
/**
208
 * This function checks if the given folder is writable.
209
 *
210
 * @param string $folder     Full path to a folder
211
 * @param bool   $suggestion Whether to show a suggestion or not
212
 *
213
 * @return string
214
 */
215
function check_writable($folder, $suggestion = false)
216
{
217
    if (is_writable($folder)) {
218
        return Display::label(get_lang('Writable'), 'success');
219
    } else {
220
        return Display::label(get_lang('Not writable'), 'important');
221
    }
222
}
223
224
/**
225
 * This function checks if the given folder is readable.
226
 *
227
 * @param string $folder     Full path to a folder
228
 * @param bool   $suggestion Whether to show a suggestion or not
229
 *
230
 * @return string
231
 */
232
function checkReadable($folder, $suggestion = false)
233
{
234
    if (is_readable($folder)) {
235
        return Display::label(get_lang('Readable'), 'success');
236
    } else {
237
        if ($suggestion) {
238
            return Display::label(get_lang('Not readable'), 'info');
239
        } else {
240
            return Display::label(get_lang('Not readable'), 'important');
241
        }
242
    }
243
}
244
245
/**
246
 * We assume this function is called from install scripts that reside inside the install folder.
247
 */
248
function set_file_folder_permissions()
249
{
250
    @chmod('.', 0755); //set permissions on install dir
251
    @chmod('..', 0755); //set permissions on parent dir of install dir
252
}
253
254
/**
255
 * This function returns the value of a parameter from the configuration file.
256
 *
257
 * WARNING - this function relies heavily on global variables $updateFromConfigFile
258
 * and $configFile, and also changes these globals. This can be rewritten.
259
 *
260
 * @param string $param      the parameter of which the value is returned
261
 * @param string $updatePath If we want to give the path rather than take it from POST
262
 *
263
 * @return string the value of the parameter
264
 *
265
 * @author Olivier Brouckaert
266
 * @author Reworked by Ivan Tcholakov, 2010
267
 */
268
function get_config_param($param, $updatePath = '')
269
{
270
    global $updateFromConfigFile;
271
    if (empty($updatePath) && !empty($_POST['updatePath'])) {
272
        $updatePath = $_POST['updatePath'];
273
    }
274
275
    if (empty($updatePath)) {
276
        $updatePath = api_get_path(SYMFONY_SYS_PATH);
277
    }
278
    $updatePath = api_add_trailing_slash(str_replace('\\', '/', realpath($updatePath)));
279
280
    if (empty($updateFromConfigFile)) {
281
        // If update from previous install was requested,
282
        if (file_exists($updatePath.'app/config/configuration.php')) {
283
            $updateFromConfigFile = 'app/config/configuration.php';
284
        } else {
285
            // Give up recovering.
286
            return null;
287
        }
288
    }
289
290
    if (file_exists($updatePath.$updateFromConfigFile) &&
291
        !is_dir($updatePath.$updateFromConfigFile)
292
    ) {
293
        require $updatePath.$updateFromConfigFile;
294
295
        if (isset($_configuration) && array_key_exists($param, $_configuration)) {
296
            return $_configuration[$param];
297
        }
298
299
        return null;
300
    }
301
302
    error_log('Config array could not be found in get_config_param()', 0);
303
304
    return null;
305
}
306
307
/**
308
 * Gets a configuration parameter from the database. Returns returns null on failure.
309
 *
310
 * @param string $param Name of param we want
311
 *
312
 * @return mixed The parameter value or null if not found
313
 */
314
function get_config_param_from_db($param = '')
315
{
316
    $param = Database::escape_string($param);
317
318
    $schemaManager = Database::getConnection()->createSchemaManager();
319
320
    if ($schemaManager->tablesExist('settings_current')) {
321
        $query = "SELECT * FROM settings_current WHERE variable = '$param'";
322
    } elseif ($schemaManager->tablesExist('settings')) {
323
        $query = "SELECT * FROM settings WHERE variable = '$param'";
324
    } else {
325
        return null;
326
    }
327
328
    if (false !== ($res = Database::query($query))) {
329
        if (Database::num_rows($res) > 0) {
330
            $row = Database::fetch_array($res);
331
            return $row['selected_value'];
332
        }
333
    }
334
335
    return null;
336
}
337
338
/**
339
 * Connect to the database and returns the entity manager.
340
 *
341
 * @param string $host
342
 * @param string $username
343
 * @param string $password
344
 * @param string $databaseName
345
 * @param int $port
346
 *
347
 * @throws \Doctrine\DBAL\Exception
348
 * @throws \Doctrine\ORM\ORMException
349
 *
350
 * @return void
351
 */
352
function connectToDatabase(
353
    $host,
354
    $username,
355
    $password,
356
    $databaseName,
357
    $port = 3306
358
): void
359
{
360
    Database::connect(
361
        [
362
            'driver' => 'pdo_mysql',
363
            'host' => $host,
364
            'port' => $port,
365
            'user' => $username,
366
            'password' => $password,
367
            'dbname' => $databaseName,
368
        ]
369
    );
370
}
371
372
/**
373
 * This function prints class=active_step $current_step=$param.
374
 *
375
 * @param int $param A step in the installer process
376
 *
377
 * @author Patrick Cool <[email protected]>, Ghent University
378
 */
379
function step_active($param)
380
{
381
    global $current_step;
382
    if ($param == $current_step) {
383
        echo 'install-steps__step--active';
384
    }
385
}
386
387
/**
388
 * This function displays the Step X of Y -.
389
 *
390
 * @return string String that says 'Step X of Y' with the right values
391
 */
392
function display_step_sequence()
393
{
394
    global $current_step;
395
396
    return get_lang('Step'.$current_step).' &ndash; ';
397
}
398
399
/**
400
 * Displays a drop down box for selection the preferred language.
401
 */
402
function display_language_selection_box($name = 'language_list', $default_language = 'en_US')
403
{
404
    // Displaying the box.
405
    return Display::select(
406
        'language_list',
407
        array_column(LanguageFixtures::getLanguages(), 'english_name', 'isocode'),
408
        $default_language,
409
        [],
410
        false
411
    );
412
}
413
414
/**
415
 * This function displays the requirements for installing Chamilo.
416
 *
417
 * @param string $updatePath         The updatePath given (if given)
418
 * @param array  $upgradeFromVersion The different subversions from version 1.9
419
 *
420
 * @author unknow
421
 * @author Patrick Cool <[email protected]>, Ghent University
422
 */
423
function display_requirements(
424
    string $installType,
425
    bool $badUpdatePath,
426
    string $updatePath = '',
427
    array $upgradeFromVersion = []
428
): array {
429
    global $_setting, $originalMemoryLimit;
430
431
    $dir = api_get_path(SYS_ARCHIVE_PATH).'temp/';
432
    $fileToCreate = 'test';
433
434
    $perms_dir = [0777, 0755, 0775, 0770, 0750, 0700];
435
    $perms_fil = [0666, 0644, 0664, 0660, 0640, 0600];
436
    $course_test_was_created = false;
437
    $dir_perm_verified = 0777;
438
439
    foreach ($perms_dir as $perm) {
440
        $r = @mkdir($dir, $perm);
441
        if (true === $r) {
442
            $dir_perm_verified = $perm;
443
            $course_test_was_created = true;
444
            break;
445
        }
446
    }
447
448
    $fil_perm_verified = 0666;
449
    $file_course_test_was_created = false;
450
    if (is_dir($dir)) {
451
        foreach ($perms_fil as $perm) {
452
            if ($file_course_test_was_created) {
453
                break;
454
            }
455
            $r = @touch($dir.'/'.$fileToCreate, $perm);
456
            if (true === $r) {
457
                $fil_perm_verified = $perm;
458
                $file_course_test_was_created = true;
459
            }
460
        }
461
    }
462
463
    @unlink($dir.'/'.$fileToCreate);
464
    @rmdir($dir);
465
466
    //  SERVER REQUIREMENTS
467
    $timezone = checkPhpSettingExists('date.timezone');
468
469
    $phpVersion = phpversion();
470
    $isVersionPassed = version_compare($phpVersion, REQUIRED_PHP_VERSION, '<=') <= 1;
471
472
    $extensions = [];
473
    $extensions[] = [
474
        'title' => get_lang('Session support'),
475
        'url' => 'https://php.net/manual/en/book.session.php',
476
        'status' => checkExtension(
477
            'session',
478
            get_lang('Yes'),
479
            get_lang('Sessions extension not available')
480
        ),
481
    ];
482
    $extensions[] = [
483
        'title' => get_lang('MySQL Functions support'),
484
        'url' => 'https://php.net/manual/en/book.mysql.php',
485
        'status' => checkExtension(
486
            'pdo_mysql',
487
            get_lang('Yes'),
488
            get_lang('MySQL extension not available')
489
        ),
490
    ];
491
    $extensions[] = [
492
        'title' => get_lang('Zip support'),
493
        'url' => 'https://php.net/manual/en/book.zip.php',
494
        'status' => checkExtension(
495
            'zip',
496
            get_lang('Yes'),
497
            get_lang('Extension not available')
498
        ),
499
    ];
500
    $extensions[] = [
501
        'title' => get_lang('Zlib support'),
502
        'url' => 'https://php.net/manual/en/book.zlib.php',
503
        'status' => checkExtension(
504
            'zlib',
505
            get_lang('Yes'),
506
            get_lang('Zlib extension not available')
507
        ),
508
    ];
509
    $extensions[] = [
510
        'title' => get_lang('Perl-compatible regular expressions support'),
511
        'url' => 'https://php.net/manual/en/book.pcre.php',
512
        'status' => checkExtension(
513
            'pcre',
514
            get_lang('Yes'),
515
            get_lang('PCRE extension not available')
516
        ),
517
    ];
518
    $extensions[] = [
519
        'title' => get_lang('XML support'),
520
        'url' => 'https://php.net/manual/en/book.xml.php',
521
        'status' => checkExtension(
522
            'xml',
523
            get_lang('Yes'),
524
            get_lang('No')
525
        ),
526
    ];
527
    $extensions[] = [
528
        'title' => get_lang('Internationalization support'),
529
        'url' => 'https://php.net/manual/en/book.intl.php',
530
        'status' => checkExtension(
531
            'intl',
532
            get_lang('Yes'),
533
            get_lang('No')
534
        ),
535
    ];
536
    $extensions[] = [
537
        'title' => get_lang('JSON support'),
538
        'url' => 'https://php.net/manual/en/book.json.php',
539
        'status' => checkExtension(
540
            'json',
541
            get_lang('Yes'),
542
            get_lang('No')
543
        ),
544
    ];
545
    $extensions[] = [
546
        'title' => get_lang('GD support'),
547
        'url' => 'https://php.net/manual/en/book.image.php',
548
        'status' => checkExtension(
549
            'gd',
550
            get_lang('Yes'),
551
            get_lang('GD Extension not available')
552
        ),
553
    ];
554
    $extensions[] = [
555
        'title' => get_lang('cURL support'),
556
        'url' => 'https://php.net/manual/en/book.curl.php',
557
        'status' => checkExtension(
558
            'curl',
559
            get_lang('Yes'),
560
            get_lang('No')
561
        ),
562
    ];
563
    $extensions[] = [
564
        'title' => get_lang('Multibyte string support'),
565
        'url' => 'https://php.net/manual/en/book.mbstring.php',
566
        'status' => checkExtension(
567
            'mbstring',
568
            get_lang('Yes'),
569
            get_lang('MBString extension not available'),
570
            true
571
        ),
572
    ];
573
    $extensions[] = [
574
        'title' => get_lang('Exif support'),
575
        'url' => 'https://php.net/manual/en/book.exif.php',
576
        'status' => checkExtension(
577
            'exif',
578
            get_lang('Yes'),
579
            get_lang('Exif extension not available'),
580
            true
581
        ),
582
    ];
583
    $extensions[] = [
584
        'title' => get_lang('Zend OpCache support'),
585
        'url' => 'https://php.net/opcache',
586
        'status' => checkExtension(
587
            'Zend OPcache',
588
            get_lang('Yes'),
589
            get_lang('No'),
590
            true,
591
            'opcache.enable'
592
        ),
593
    ];
594
    $extensions[] = [
595
        'title' => get_lang('APCu support'),
596
        'url' => 'https://php.net/apcu',
597
        'status' => checkExtension(
598
            'apcu',
599
            get_lang('Yes'),
600
            get_lang('No'),
601
            true,
602
            'apc.enabled'
603
        ),
604
    ];
605
    $extensions[] = [
606
        'title' => get_lang('Iconv support'),
607
        'url' => 'https://php.net/manual/en/book.iconv.php',
608
        'status' => checkExtension(
609
            'iconv',
610
            get_lang('Yes'),
611
            get_lang('No'),
612
            true
613
        ),
614
    ];
615
    $extensions[] = [
616
        'title' => get_lang('LDAP support'),
617
        'url' => 'https://php.net/manual/en/book.ldap.php',
618
        'status' => checkExtension(
619
            'ldap',
620
            get_lang('Yes'),
621
            get_lang('LDAP Extension not available'),
622
            true
623
        ),
624
    ];
625
    $extensions[] = [
626
        'title' => get_lang('Xapian support'),
627
        'url' => 'https://xapian.org/',
628
        'status' => checkExtension(
629
            'xapian',
630
            get_lang('Yes'),
631
            get_lang('No'),
632
            true
633
        ),
634
    ];
635
636
    // RECOMMENDED SETTINGS
637
    // Note: these are the settings for Joomla, does this also apply for Chamilo?
638
    // Note: also add upload_max_filesize here so that large uploads are possible
639
    $phpIni = [];
640
    $phpIni[] = [
641
        'title' => 'Display Errors',
642
        'url' => 'https://php.net/manual/ref.errorfunc.php#ini.display-errors',
643
        'recommended' => 'OFF',
644
        'current' => checkPhpSetting('display_errors', 'OFF'),
645
    ];
646
    $phpIni[] = [
647
        'title' => 'File Uploads',
648
        'url' => 'https://php.net/manual/ini.core.php#ini.file-uploads',
649
        'recommended' => 'ON',
650
        'current' => checkPhpSetting('file_uploads', 'ON'),
651
    ];
652
    $phpIni[] = [
653
        'title' => 'Session auto start',
654
        'url' => 'https://php.net/manual/ref.session.php#ini.session.auto-start',
655
        'recommended' => 'OFF',
656
        'current' => checkPhpSetting('session.auto_start', 'OFF'),
657
    ];
658
    $phpIni[] = [
659
        'title' => 'Short Open Tag',
660
        'url' => 'https://php.net/manual/ini.core.php#ini.short-open-tag',
661
        'recommended' => 'OFF',
662
        'current' => checkPhpSetting('short_open_tag', 'OFF'),
663
    ];
664
    $phpIni[] = [
665
        'title' => 'Cookie HTTP Only',
666
        'url' => 'https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-httponly',
667
        'recommended' => 'ON',
668
        'current' => checkPhpSetting('session.cookie_httponly', 'ON'),
669
    ];
670
    $phpIni[] = [
671
        'title' => 'Maximum upload file size',
672
        'url' => 'https://php.net/manual/ini.core.php#ini.upload-max-filesize',
673
        'recommended' => '>= '.REQUIRED_MIN_UPLOAD_MAX_FILESIZE.'M',
674
        'current' => compare_setting_values(ini_get('upload_max_filesize'), REQUIRED_MIN_UPLOAD_MAX_FILESIZE),
675
    ];
676
    $phpIni[] = [
677
        'title' => 'Maximum post size',
678
        'url' => 'https://php.net/manual/ini.core.php#ini.post-max-size',
679
        'recommended' => '>= '.REQUIRED_MIN_POST_MAX_SIZE.'M',
680
        'current' => compare_setting_values(ini_get('post_max_size'), REQUIRED_MIN_POST_MAX_SIZE),
681
    ];
682
    $phpIni[] = [
683
        'title' => 'Memory Limit',
684
        'url' => 'https://www.php.net/manual/en/ini.core.php#ini.memory-limit',
685
        'recommended' => '>= '.REQUIRED_MIN_MEMORY_LIMIT.'M',
686
        'current' => compare_setting_values($originalMemoryLimit, REQUIRED_MIN_MEMORY_LIMIT),
687
    ];
688
689
    // DIRECTORY AND FILE PERMISSIONS
690
    $_SESSION['permissions_for_new_directories'] = $_setting['permissions_for_new_directories'] = $dir_perm_verified;
691
    $_SESSION['permissions_for_new_files'] = $_setting['permissions_for_new_files'] = $fil_perm_verified;
692
693
    $dirPerm = '0'.decoct($dir_perm_verified);
694
    $filePerm = '0'.decoct($fil_perm_verified);
695
696
    $pathPermissions = [];
697
698
    if (file_exists(api_get_path(SYS_CODE_PATH).'inc/conf/configuration.php')) {
699
        $pathPermissions[] = [
700
            'requirement' => api_get_path(SYS_CODE_PATH).'inc/conf',
701
            'status' => is_writable(api_get_path(SYS_CODE_PATH).'inc/conf'),
702
        ];
703
    }
704
    $basePath = api_get_path(SYMFONY_SYS_PATH);
705
706
    $pathPermissions[] = [
707
        'item' => $basePath.'var/',
708
        'status' => is_writable($basePath.'var'),
709
    ];
710
    $pathPermissions[] = [
711
        'item' => $basePath.'config/',
712
        'status' => is_writable($basePath.'config'),
713
    ];
714
    $pathPermissions[] = [
715
        'item' => $basePath.'.env',
716
        'status' => checkCanCreateFile($basePath.'.env'),
717
    ];
718
    $pathPermissions[] = [
719
        'item' => get_lang('Permissions for new directories'),
720
        'status' => $dirPerm,
721
    ];
722
    $pathPermissions[] = [
723
        'item' => get_lang('Permissions for new files'),
724
        'status' => $filePerm,
725
    ];
726
727
    $notWritable = [];
728
    $deprecatedToRemove = [];
729
730
    $error = false;
731
732
    if ('update' !== $installType || !empty($updatePath) && !$badUpdatePath) {
733
        // First, attempt to set writing permissions if we don't have them yet
734
        //$perm = api_get_permissions_for_new_directories();
735
        $perm = octdec('0777');
736
        //$perm_file = api_get_permissions_for_new_files();
737
        $perm_file = octdec('0666');
738
739
        if (!$course_test_was_created) {
740
            error_log('Installer: Could not create test course - Make sure permissions are fine.');
741
            $error = true;
742
        }
743
744
        $checked_writable = api_get_path(CONFIGURATION_PATH).'configuration.php';
745
        if (file_exists($checked_writable) && !is_writable($checked_writable)) {
746
            $notWritable[] = $checked_writable;
747
            @chmod($checked_writable, $perm_file);
748
        }
749
750
        // Second, if this fails, report an error
751
        //--> The user would have to adjust the permissions manually
752
        if (count($notWritable) > 0) {
753
            error_log('Installer: At least one needed directory or file is not writeable');
754
            $error = true;
755
        }
756
757
        $deprecated = [
758
            api_get_path(SYS_CODE_PATH).'exercice/',
759
            api_get_path(SYS_CODE_PATH).'newscorm/',
760
            api_get_path(SYS_PLUGIN_PATH).'ticket/',
761
            api_get_path(SYS_PLUGIN_PATH).'skype/',
762
        ];
763
764
        foreach ($deprecated as $deprecatedDirectory) {
765
            if (!is_dir($deprecatedDirectory)) {
766
                continue;
767
            }
768
            $deprecatedToRemove[] = $deprecatedDirectory;
769
        }
770
    }
771
772
    return [
773
        'timezone' => $timezone,
774
        'isVersionPassed' => $isVersionPassed,
775
        'phpVersion' => $phpVersion,
776
        'extensions' => $extensions,
777
        'phpIni' => $phpIni,
778
        'pathPermissions' => $pathPermissions,
779
        'step2_update_6' => isset($_POST['step2_update_6']),
780
        'notWritable' => $notWritable,
781
        'existsConfigurationFile' => false,
782
        'deprecatedToRemove' => $deprecatedToRemove,
783
        'installError' => $error,
784
    ];
785
}
786
787
/**
788
 * Displays the license (GNU GPL) as step 2, with
789
 * - an "I accept" button named step3 to proceed to step 3;
790
 * - a "Back" button named step1 to go back to the first step.
791
 */
792
function display_license_agreement(): array
793
{
794
    $license = api_htmlentities(@file_get_contents(api_get_path(SYMFONY_SYS_PATH).'public/documentation/license.txt'));
795
796
    $activtiesList = [
797
        ['Advertising/Marketing/PR'],
798
        ['Agriculture/Forestry'],
799
        ['Architecture'],
800
        ['Banking/Finance'],
801
        ['Biotech/Pharmaceuticals'],
802
        ['Business Equipment'],
803
        ['Business Services'],
804
        ['Construction'],
805
        ['Consulting/Research'],
806
        ['Education'],
807
        ['Engineering'],
808
        ['Environmental'],
809
        ['Government'],
810
        ['Health Care'],
811
        ['Hospitality/Lodging/Travel'],
812
        ['Insurance'],
813
        ['Legal'],
814
        ['Manufacturing'],
815
        ['Media/Entertainment'],
816
        ['Mortgage'],
817
        ['Non-Profit'],
818
        ['Real Estate'],
819
        ['Restaurant'],
820
        ['Retail'],
821
        ['Shipping/Transportation'],
822
        ['Technology'],
823
        ['Telecommunications'],
824
        ['Other'],
825
    ];
826
827
    $rolesList = [
828
        ['Administration'],
829
        ['CEO/President/ Owner'],
830
        ['CFO'],
831
        ['CIO/CTO'],
832
        ['Consultant'],
833
        ['Customer Service'],
834
        ['Engineer/Programmer'],
835
        ['Facilities/Operations'],
836
        ['Finance/ Accounting Manager'],
837
        ['Finance/ Accounting Staff'],
838
        ['General Manager'],
839
        ['Human Resources'],
840
        ['IS/IT Management'],
841
        ['IS/ IT Staff'],
842
        ['Marketing Manager'],
843
        ['Marketing Staff'],
844
        ['Partner/Principal'],
845
        ['Purchasing Manager'],
846
        ['Sales/ Business Dev. Manager'],
847
        ['Sales/ Business Dev.'],
848
        ['Vice President/Senior Manager'],
849
        ['Other'],
850
    ];
851
852
    $countriesList = array_map(
853
        fn ($country) => [$country],
854
        get_countries_list_from_array()
855
    );
856
857
    $languagesList = [
858
        ['bulgarian', 'Bulgarian'],
859
        ['indonesian', 'Bahasa Indonesia'],
860
        ['bosnian', 'Bosanski'],
861
        ['german', 'Deutsch'],
862
        ['english', 'English'],
863
        ['spanish', 'Spanish'],
864
        ['french', 'Français'],
865
        ['italian', 'Italian'],
866
        ['hungarian', 'Magyar'],
867
        ['dutch', 'Nederlands'],
868
        ['brazilian', 'Português do Brasil'],
869
        ['portuguese', 'Português europeu'],
870
        ['slovenian', 'Slovenčina'],
871
    ];
872
873
    return [
874
        'license' => $license,
875
        'activitiesList' => $activtiesList,
876
        'rolesList' => $rolesList,
877
        'countriesList' => $countriesList,
878
        'languagesList' => $languagesList,
879
    ];
880
}
881
882
/**
883
 * Displays a parameter in a table row.
884
 * Used by the display_database_settings_form function.
885
 *
886
 * @param   string  Type of install
887
 * @param   string  Name of parameter
888
 * @param   string  Field name (in the HTML form)
889
 * @param   string  Field value
890
 * @param   string  Extra notice (to show on the right side)
891
 * @param   bool Whether to display in update mode
892
 * @param   string  Additional attribute for the <tr> element
893
 */
894
function displayDatabaseParameter(
895
    $installType,
896
    $parameterName,
897
    $formFieldName,
898
    $parameterValue,
899
    $extra_notice,
900
    $displayWhenUpdate = true
901
) {
902
    echo "<dt class='col-sm-4'>$parameterName</dt>";
903
    echo '<dd class="col-sm-8">';
904
    if (INSTALL_TYPE_UPDATE == $installType && $displayWhenUpdate) {
905
        echo '<input
906
                type="hidden"
907
                name="'.$formFieldName.'"
908
                id="'.$formFieldName.'"
909
                value="'.api_htmlentities($parameterValue).'" />'.$parameterValue;
910
    } else {
911
        $inputType = 'dbPassForm' === $formFieldName ? 'password' : 'text';
912
        //Slightly limit the length of the database prefix to avoid having to cut down the databases names later on
913
        $maxLength = 'dbPrefixForm' === $formFieldName ? '15' : MAX_FORM_FIELD_LENGTH;
914
        if (INSTALL_TYPE_UPDATE == $installType) {
915
            echo '<input
916
                type="hidden" name="'.$formFieldName.'" id="'.$formFieldName.'"
917
                value="'.api_htmlentities($parameterValue).'" />';
918
            echo api_htmlentities($parameterValue);
919
        } else {
920
            echo '<input
921
                        type="'.$inputType.'"
922
                        class="form-control"
923
                        size="'.DATABASE_FORM_FIELD_DISPLAY_LENGTH.'"
924
                        maxlength="'.$maxLength.'"
925
                        name="'.$formFieldName.'"
926
                        id="'.$formFieldName.'"
927
                        value="'.api_htmlentities($parameterValue).'" />
928
                    '.$extra_notice.'
929
                  ';
930
        }
931
    }
932
    echo '</dd>';
933
}
934
935
/**
936
 * Displays step 3 - a form where the user can enter the installation settings
937
 * regarding the databases - login and password, names, prefixes, single
938
 * or multiple databases, tracking or not...
939
 */
940
function display_database_settings_form(
941
    string $installType,
942
    string $dbHostForm,
943
    string $dbUsernameForm,
944
    string $dbPassForm,
945
    string $dbNameForm,
946
    int $dbPortForm = 3306
947
): array {
948
    if ('update' === $installType) {
949
        $dbHostForm = get_config_param('db_host');
950
        $dbUsernameForm = get_config_param('db_user');
951
        $dbPassForm = get_config_param('db_password');
952
        $dbNameForm = get_config_param('main_database');
953
        $dbPortForm = get_config_param('db_port');
954
    }
955
956
    $databaseExists = false;
957
    $databaseConnectionError = '';
958
    $connectionParams = null;
959
960
    try {
961
        if ('update' === $installType) {
962
            connectToDatabase(
963
                $dbHostForm,
964
                $dbUsernameForm,
965
                $dbPassForm,
966
                $dbNameForm,
967
                $dbPortForm
968
            );
969
970
            $manager = Database::getManager();
971
            $connection = $manager->getConnection();
972
            $connection->connect();
973
            $schemaManager = $connection->getSchemaManager();
974
975
            // Test create/alter/drop table
976
            $table = 'zXxTESTxX_'.mt_rand(0, 1000);
977
            $sql = "CREATE TABLE $table (id INT AUTO_INCREMENT NOT NULL, name varchar(255), PRIMARY KEY(id))";
978
            $connection->executeQuery($sql);
979
            $tableCreationWorks = false;
980
            $tableDropWorks = false;
981
            if ($schemaManager->tablesExist($table)) {
982
                $sql = "ALTER TABLE $table ADD COLUMN name2 varchar(140) ";
983
                $connection->executeQuery($sql);
984
                $schemaManager->dropTable($table);
985
                $tableDropWorks = false === $schemaManager->tablesExist($table);
986
            }
987
        } else {
988
            connectToDatabase(
989
                $dbHostForm,
990
                $dbUsernameForm,
991
                $dbPassForm,
992
                null,
993
                $dbPortForm
994
            );
995
996
            $manager = Database::getManager();
997
            $schemaManager = $manager->getConnection()->createSchemaManager();
998
            $databases = $schemaManager->listDatabases();
999
            $databaseExists = in_array($dbNameForm, $databases);
1000
        }
1001
    } catch (Exception $e) {
1002
        $databaseConnectionError = $e->getMessage();
1003
        $manager = null;
1004
    }
1005
1006
    if ($manager && $manager->getConnection()->isConnected()) {
1007
        $connectionParams = $manager->getConnection()->getParams();
1008
    }
1009
1010
    return [
1011
        'dbHostForm' => $dbHostForm,
1012
        'dbPortForm' => $dbPortForm,
1013
        'dbUsernameForm' => $dbUsernameForm,
1014
        'dbPassForm' => $dbPassForm,
1015
        'dbNameForm' => $dbNameForm,
1016
        'examplePassword' => api_generate_password(8, false),
1017
        'dbExists' => $databaseExists,
1018
        'dbConnError' => $databaseConnectionError,
1019
        'connParams' => $connectionParams,
1020
    ];
1021
}
1022
1023
/**
1024
 * Displays a parameter in a table row.
1025
 * Used by the display_configuration_settings_form function.
1026
 *
1027
 * @param string $installType
1028
 * @param string $parameterName
1029
 * @param string $formFieldName
1030
 * @param string $parameterValue
1031
 * @param string $displayWhenUpdate
1032
 *
1033
 * @return string
1034
 */
1035
function display_configuration_parameter(
1036
    $installType,
1037
    $parameterName,
1038
    $formFieldName,
1039
    $parameterValue,
1040
    $displayWhenUpdate = 'true'
1041
) {
1042
    $html = '<div class="form-group row">';
1043
    $html .= '<label class="col-sm-6 p-2 control-label">'.$parameterName.'</label>';
1044
    if (INSTALL_TYPE_UPDATE == $installType && $displayWhenUpdate) {
1045
        $html .= '<input
1046
            type="hidden"
1047
            name="'.$formFieldName.'"
1048
            value="'.api_htmlentities($parameterValue, ENT_QUOTES).'" />'.$parameterValue;
1049
    } else {
1050
        $html .= '<div class="col-sm-6">
1051
                    <input
1052
                        class="form-control"
1053
                        type="text"
1054
                        size="'.FORM_FIELD_DISPLAY_LENGTH.'"
1055
                        maxlength="'.MAX_FORM_FIELD_LENGTH.'"
1056
                        name="'.$formFieldName.'"
1057
                        value="'.api_htmlentities($parameterValue, ENT_QUOTES).'" />
1058
                    '.'</div>';
1059
    }
1060
    $html .= '</div>';
1061
1062
    return $html;
1063
}
1064
1065
/**
1066
 * Displays step 4 of the installation - configuration settings about Chamilo itself.
1067
 */
1068
function display_configuration_settings_form(
1069
    string $installType,
1070
    string $urlForm,
1071
    string $languageForm,
1072
    string $emailForm,
1073
    string $adminFirstName,
1074
    string $adminLastName,
1075
    string $adminPhoneForm,
1076
    string $campusForm,
1077
    string $institutionForm,
1078
    string $institutionUrlForm,
1079
    string $encryptPassForm,
1080
    string $allowSelfReg,
1081
    string $allowSelfRegProf,
1082
    string $loginForm,
1083
    string $passForm
1084
): array {
1085
    if ('update' !== $installType && empty($languageForm)) {
1086
        $languageForm = $_SESSION['install_language'];
1087
    }
1088
1089
    $stepData = [];
1090
1091
    if ('update' === $installType) {
1092
        $stepData['rootWeb'] = get_config_param('root_web');
1093
        $stepData['rootSys'] = get_config_param('root_sys');
1094
        $stepData['systemVersion'] = get_config_param('system_version');
1095
    }
1096
1097
    $stepData['loginForm'] = $loginForm;
1098
    $stepData['passForm'] = $passForm;
1099
    $stepData['adminFirstName'] = $adminFirstName;
1100
    $stepData['adminLastName'] = $adminLastName;
1101
    $stepData['emailForm'] = $emailForm;
1102
    $stepData['adminPhoneForm'] = $adminPhoneForm;
1103
    $stepData['languageForm'] = $languageForm;
1104
    $stepData['urlForm'] = $urlForm;
1105
    $stepData['campusForm'] = $campusForm;
1106
    $stepData['institutionForm'] = $institutionForm;
1107
    $stepData['institutionUrlForm'] = $institutionUrlForm;
1108
    $stepData['encryptPassForm'] = $encryptPassForm;
1109
    $stepData['allowSelfReg'] = $allowSelfReg;
1110
    $stepData['allowSelfRegProf'] = $allowSelfRegProf;
1111
1112
    return $stepData;
1113
}
1114
1115
/**
1116
 * This function return countries list from array (hardcoded).
1117
 *
1118
 * @param bool $combo (Optional) True for returning countries list with select html
1119
 *
1120
 * @return array|string countries list
1121
 */
1122
function get_countries_list_from_array($combo = false)
1123
{
1124
    $a_countries = [
1125
        'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan',
1126
        'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi',
1127
        'Cambodia', 'Cameroon', 'Canada', 'Cape Verde', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombi', 'Comoros', 'Congo (Brazzaville)', 'Congo', 'Costa Rica', "Cote d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czech Republic',
1128
        'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic',
1129
        'East Timor (Timor Timur)', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Ethiopia',
1130
        'Fiji', 'Finland', 'France',
1131
        'Gabon', 'Gambia, The', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana',
1132
        'Haiti', 'Honduras', 'Hungary',
1133
        'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy',
1134
        'Jamaica', 'Japan', 'Jordan',
1135
        'Kazakhstan', 'Kenya', 'Kiribati', 'Korea, North', 'Korea, South', 'Kuwait', 'Kyrgyzstan',
1136
        'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg',
1137
        'Macedonia', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Morocco', 'Mozambique', 'Myanmar',
1138
        'Namibia', 'Nauru', 'Nepa', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'Norway',
1139
        'Oman',
1140
        'Pakistan', 'Palau', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal',
1141
        'Qatar',
1142
        'Romania', 'Russia', 'Rwanda',
1143
        'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia and Montenegro', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Swaziland', 'Sweden', 'Switzerland', 'Syria',
1144
        'Taiwan', 'Tajikistan', 'Tanzania', 'Thailand', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu',
1145
        'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States', 'Uruguay', 'Uzbekistan',
1146
        'Vanuatu', 'Vatican City', 'Venezuela', 'Vietnam',
1147
        'Yemen',
1148
        'Zambia', 'Zimbabwe',
1149
    ];
1150
    $options = array_combine($a_countries, $a_countries);
1151
    if ($combo) {
1152
        return Display::select(
1153
            'country',
1154
            $options + ['' => get_lang('Select one')],
1155
            '',
1156
            ['id' => 'country'],
1157
            false
1158
        );
1159
    }
1160
1161
    return $a_countries;
1162
}
1163
1164
/**
1165
 * Lock settings that can't be changed in other portals.
1166
 */
1167
function lockSettings()
1168
{
1169
    $settings = api_get_locked_settings();
1170
    $table = Database::get_main_table(TABLE_MAIN_SETTINGS);
1171
    foreach ($settings as $setting) {
1172
        $sql = "UPDATE $table SET access_url_locked = 1 WHERE variable  = '$setting'";
1173
        Database::query($sql);
1174
    }
1175
}
1176
1177
/**
1178
 * Update dir values.
1179
 */
1180
function updateDirAndFilesPermissions()
1181
{
1182
    $table = Database::get_main_table(TABLE_MAIN_SETTINGS);
1183
    $permissions_for_new_directories = isset($_SESSION['permissions_for_new_directories']) ? $_SESSION['permissions_for_new_directories'] : 0770;
1184
    $permissions_for_new_files = isset($_SESSION['permissions_for_new_files']) ? $_SESSION['permissions_for_new_files'] : 0660;
1185
    // use decoct() to store as string
1186
    Database::update(
1187
        $table,
1188
        ['selected_value' => '0'.decoct($permissions_for_new_directories)],
1189
        ['variable = ?' => 'permissions_for_new_directories']
1190
    );
1191
1192
    Database::update(
1193
        $table,
1194
        ['selected_value' => '0'.decoct($permissions_for_new_files)],
1195
        ['variable = ?' => 'permissions_for_new_files']
1196
    );
1197
1198
    if (isset($_SESSION['permissions_for_new_directories'])) {
1199
        unset($_SESSION['permissions_for_new_directories']);
1200
    }
1201
1202
    if (isset($_SESSION['permissions_for_new_files'])) {
1203
        unset($_SESSION['permissions_for_new_files']);
1204
    }
1205
}
1206
1207
function compare_setting_values(string $current_value, string $wanted_value): array
1208
{
1209
    $tail = substr($current_value, -1, 1);
1210
    $current_value_string = $current_value;
1211
    switch ($tail) {
1212
        case 'T':
1213
            $current_value = ((float) substr($current_value, 0, -1)) * 1024 * 1024;
1214
            break;
1215
        case 'G':
1216
            $current_value = ((float) substr($current_value, 0, -1)) * 1024;
1217
            break;
1218
        case 'M':
1219
        default:
1220
            $current_value = (float) $current_value;
1221
        break;
1222
    }
1223
    $wanted_value = (float) $wanted_value;
1224
1225
    return $current_value >= $wanted_value
1226
        ? ['severity' => 'success', 'value' => $current_value_string]
1227
        : ['severity' => 'danger', 'value' => $current_value_string];
1228
}
1229
1230
/**
1231
 * Save settings values.
1232
 *
1233
 * @param string $organizationName
1234
 * @param string $organizationUrl
1235
 * @param string $siteName
1236
 * @param string $adminEmail
1237
 * @param string $adminLastName
1238
 * @param string $adminFirstName
1239
 * @param string $language
1240
 * @param string $allowRegistration
1241
 * @param string $allowTeacherSelfRegistration
1242
 * @param string $installationProfile          The name of an installation profile file in main/install/profiles/
1243
 */
1244
function installSettings(
1245
    $organizationName,
1246
    $organizationUrl,
1247
    $siteName,
1248
    $adminEmail,
1249
    $adminLastName,
1250
    $adminFirstName,
1251
    $language,
1252
    $allowRegistration,
1253
    $allowTeacherSelfRegistration,
1254
    $installationProfile = '',
1255
    $mailerDsn = '',
1256
    $mailerFromEmail = '',
1257
    $mailerFromName = '',
1258
) {
1259
    error_log('installSettings');
1260
    $allowTeacherSelfRegistration = $allowTeacherSelfRegistration ? 'true' : 'false';
1261
1262
    $settings = [
1263
        'institution' => $organizationName,
1264
        'institution_url' => $organizationUrl,
1265
        'site_name' => $siteName,
1266
        'administrator_email' => $adminEmail,
1267
        'administrator_surname' => $adminLastName,
1268
        'administrator_name' => $adminFirstName,
1269
        'platform_language' => $language,
1270
        'allow_registration' => $allowRegistration,
1271
        'allow_registration_as_teacher' => $allowTeacherSelfRegistration,
1272
        'mailer_dsn' => $mailerDsn,
1273
        'mailer_from_email' => $mailerFromEmail,
1274
        'mailer_from_name' => $mailerFromName,
1275
    ];
1276
1277
    foreach ($settings as $variable => $value) {
1278
        $sql = "UPDATE settings
1279
                SET selected_value = '$value'
1280
                WHERE variable = '$variable'";
1281
        Database::query($sql);
1282
    }
1283
    installProfileSettings($installationProfile);
1284
}
1285
1286
/**
1287
 * Executes DB changes based in the classes defined in
1288
 * /src/CoreBundle/Migrations/Schema/V200/*.
1289
 *
1290
 * @return bool
1291
 */
1292
function migrate(EntityManager $manager)
1293
{
1294
    $debug = true;
1295
    $connection = $manager->getConnection();
1296
    $to = null; // if $to == null then schema will be migrated to latest version
1297
1298
    // Loading migration configuration.
1299
    $config = new PhpFile('./migrations.php');
1300
    $dependency = DependencyFactory::fromConnection($config, new ExistingConnection($connection));
1301
1302
    // Check if old "version" table exists from 1.11.x, use new version.
1303
    $schema = $manager->getConnection()->getSchemaManager();
1304
    $dropOldVersionTable = false;
1305
    if ($schema->tablesExist('version')) {
1306
        $columns = $schema->listTableColumns('version');
1307
        if (in_array('id', array_keys($columns), true)) {
1308
            $dropOldVersionTable = true;
1309
        }
1310
    }
1311
1312
    if ($dropOldVersionTable) {
1313
        error_log('Drop version table');
1314
        $schema->dropTable('version');
1315
    }
1316
1317
    // Creates "version" table.
1318
    $dependency->getMetadataStorage()->ensureInitialized();
1319
1320
    // Loading migrations.
1321
    $migratorConfigurationFactory = $dependency->getConsoleInputMigratorConfigurationFactory();
1322
    $result = '';
1323
    $input = new Symfony\Component\Console\Input\StringInput($result);
1324
    $migratorConfiguration = $migratorConfigurationFactory->getMigratorConfiguration($input);
1325
    $migrator = $dependency->getMigrator();
1326
    $planCalculator = $dependency->getMigrationPlanCalculator();
1327
    $migrations = $planCalculator->getMigrations();
1328
    $lastVersion = $migrations->getLast();
1329
1330
    $plan = $dependency->getMigrationPlanCalculator()->getPlanUntilVersion($lastVersion->getVersion());
1331
1332
    foreach ($plan->getItems() as $item) {
1333
        error_log("Version to be executed: ".$item->getVersion());
1334
        $item->getMigration()->setEntityManager($manager);
1335
        $item->getMigration()->setContainer(Container::$container);
1336
    }
1337
1338
    // Execute migration!!
1339
    /** @var $migratedVersions */
1340
    $versions = $migrator->migrate($plan, $migratorConfiguration);
1341
1342
    if ($debug) {
1343
        /** @var Query[] $queries */
1344
        $versionCounter = 1;
1345
        foreach ($versions as $version => $queries) {
1346
            $total = count($queries);
1347
            //echo '----------------------------------------------<br />';
1348
            $message = "VERSION: $version";
1349
            //echo "$message<br/>";
1350
            error_log('-------------------------------------');
1351
            error_log($message);
1352
            $counter = 1;
1353
            foreach ($queries as $query) {
1354
                $sql = $query->getStatement();
1355
                //echo "<code>$sql</code><br>";
1356
                error_log("$counter/$total : $sql");
1357
                $counter++;
1358
            }
1359
            $versionCounter++;
1360
        }
1361
        //echo '<br/>DONE!<br />';
1362
        error_log('DONE!');
1363
    }
1364
1365
    return true;
1366
}
1367
1368
/**
1369
 * @param string $distFile
1370
 * @param string $envFile
1371
 * @param array  $params
1372
 */
1373
function updateEnvFile($distFile, $envFile, $params)
1374
{
1375
    $requirements = [
1376
        'DATABASE_HOST',
1377
        'DATABASE_PORT',
1378
        'DATABASE_NAME',
1379
        'DATABASE_USER',
1380
        'DATABASE_PASSWORD',
1381
        'APP_INSTALLED',
1382
        'APP_ENCRYPT_METHOD',
1383
        'APP_SECRET',
1384
        'DB_MANAGER_ENABLED',
1385
        'SOFTWARE_NAME',
1386
        'SOFTWARE_URL',
1387
        'DENY_DELETE_USERS',
1388
        'HOSTING_TOTAL_SIZE_LIMIT',
1389
        'THEME_FALLBACK',
1390
        'PACKAGER',
1391
        'DEFAULT_TEMPLATE',
1392
        'ADMIN_CHAMILO_ANNOUNCEMENTS_DISABLE',
1393
    ];
1394
1395
    foreach ($requirements as $requirement) {
1396
        if (!isset($params['{{'.$requirement.'}}'])) {
1397
            throw new \Exception("The parameter $requirement is needed in order to edit the .env file");
1398
        }
1399
    }
1400
1401
    $contents = file_get_contents($distFile);
1402
    $contents = str_replace(array_keys($params), array_values($params), $contents);
1403
    file_put_contents($envFile, $contents);
1404
    error_log("File env saved here: $envFile");
1405
}
1406
1407
function installTools($container, $manager, $upgrade = false)
1408
{
1409
    error_log('installTools');
1410
    // Install course tools (table "tool")
1411
    /** @var ToolChain $toolChain */
1412
    $toolChain = $container->get(ToolChain::class);
1413
    $toolChain->createTools();
1414
}
1415
1416
/**
1417
 * @param SymfonyContainer $container
1418
 * @param bool             $upgrade
1419
 */
1420
function installSchemas($container, $upgrade = false)
1421
{
1422
    error_log('installSchemas');
1423
    $settingsManager = $container->get(Chamilo\CoreBundle\Settings\SettingsManager::class);
1424
1425
    $urlRepo = $container->get(AccessUrlRepository::class);
1426
    $accessUrl = $urlRepo->find(1);
1427
    if (null === $accessUrl) {
1428
        $em = Database::getManager();
1429
1430
        // Creating AccessUrl.
1431
        $accessUrl = new AccessUrl();
1432
        $accessUrl
1433
            ->setUrl(AccessUrl::DEFAULT_ACCESS_URL)
1434
            ->setDescription('')
1435
            ->setActive(1)
1436
            ->setCreatedBy(1)
1437
        ;
1438
        $em->persist($accessUrl);
1439
        $em->flush();
1440
1441
        error_log('AccessUrl created');
1442
    }
1443
1444
    if ($upgrade) {
1445
        error_log('Upgrade settings');
1446
        $settingsManager->updateSchemas($accessUrl);
1447
    } else {
1448
        error_log('Install settings');
1449
        // Installing schemas (filling settings table)
1450
        $settingsManager->installSchemas($accessUrl);
1451
    }
1452
}
1453
1454
/**
1455
 * @param SymfonyContainer $container
1456
 */
1457
function upgradeWithContainer($container)
1458
{
1459
    Container::setContainer($container);
1460
    Container::setLegacyServices($container);
1461
    error_log('setLegacyServices');
1462
    $manager = Database::getManager();
1463
1464
    /** @var GroupRepository $repo */
1465
    $repo = $container->get(GroupRepository::class);
1466
    $repo->createDefaultGroups();
1467
1468
    // @todo check if adminId = 1
1469
    installTools($container, $manager, true);
1470
    installSchemas($container, true);
1471
}
1472
1473
/**
1474
 * After the schema was created (table creation), the function adds
1475
 * admin/platform information.
1476
 *
1477
 * @param \Psr\Container\ContainerInterface $container
1478
 * @param string                            $sysPath
1479
 * @param string                            $encryptPassForm
1480
 * @param string                            $passForm
1481
 * @param string                            $adminLastName
1482
 * @param string                            $adminFirstName
1483
 * @param string                            $loginForm
1484
 * @param string                            $emailForm
1485
 * @param string                            $adminPhoneForm
1486
 * @param string                            $languageForm
1487
 * @param string                            $institutionForm
1488
 * @param string                            $institutionUrlForm
1489
 * @param string                            $siteName
1490
 * @param string                            $allowSelfReg
1491
 * @param string                            $allowSelfRegProf
1492
 * @param string                            $installationProfile Installation profile, if any was provided
1493
 */
1494
function finishInstallationWithContainer(
1495
    $container,
1496
    $sysPath,
1497
    $encryptPassForm,
1498
    $passForm,
1499
    $adminLastName,
1500
    $adminFirstName,
1501
    $loginForm,
1502
    $emailForm,
1503
    $adminPhoneForm,
1504
    $languageForm,
1505
    $institutionForm,
1506
    $institutionUrlForm,
1507
    $siteName,
1508
    $allowSelfReg,
1509
    $allowSelfRegProf,
1510
    $installationProfile = '',
1511
    $mailerDsn,
1512
    $mailerFromEmail,
1513
    $mailerFromName,
1514
    \Chamilo\Kernel $kernel
1515
) {
1516
    Container::setContainer($container);
1517
    Container::setLegacyServices($container);
1518
1519
    $timezone = api_get_timezone();
1520
1521
    $repo = Container::getUserRepository();
1522
    /** @var User $admin */
1523
    $admin = $repo->findOneBy(['username' => 'admin']);
1524
1525
    $em = Container::getEntityManager();
1526
    $accessUrlRepo = $em->getRepository(AccessUrl::class);
1527
1528
    $accessUrl = $accessUrlRepo->findOneBy([]);
1529
1530
    if (!$accessUrl) {
1531
        $accessUrl = Container::getAccessUrlUtil()->getCurrent();
1532
        $em->persist($accessUrl);
1533
        $em->flush();
1534
    }
1535
1536
    $admin
1537
        ->setLastname($adminLastName)
1538
        ->setFirstname($adminFirstName)
1539
        ->setUsername($loginForm)
1540
        ->setStatus(1)
1541
        ->setPlainPassword($passForm)
1542
        ->setEmail($emailForm)
1543
        ->setOfficialCode('ADMIN')
1544
        ->addAuthSourceByAuthentication(UserAuthSource::PLATFORM, $accessUrl)
1545
        ->setPhone($adminPhoneForm)
1546
        ->setLocale($languageForm)
1547
        ->setTimezone($timezone)
1548
    ;
1549
1550
    $repo->updateUser($admin);
1551
1552
    /** @var User $anonUser */
1553
    $anonUser = $repo->findOneBy(['username' => 'anon']);
1554
    $anonUser->addAuthSourceByAuthentication(UserAuthSource::PLATFORM, $accessUrl);
1555
1556
    $repo->updateUser($anonUser);
1557
1558
    /** @var User $fallbackUser */
1559
    $fallbackUser = $repo->findOneBy(['username' => 'fallback_user']);
1560
    $fallbackUser->addAuthSourceByAuthentication(UserAuthSource::PLATFORM, $accessUrl);
1561
1562
    $repo->updateUser($fallbackUser);
1563
1564
    // Set default language
1565
    Database::update(
1566
        Database::get_main_table(TABLE_MAIN_LANGUAGE),
1567
        ['available' => 1],
1568
        ['english_name = ?' => $languageForm]
1569
    );
1570
1571
    // Install settings
1572
    installSettings(
1573
        $institutionForm,
1574
        $institutionUrlForm,
1575
        $siteName,
1576
        $emailForm,
1577
        $adminLastName,
1578
        $adminFirstName,
1579
        $languageForm,
1580
        $allowSelfReg,
1581
        $allowSelfRegProf,
1582
        $installationProfile,
1583
        $mailerDsn,
1584
        $mailerFromEmail ?: $emailForm,
1585
        $mailerFromName,
1586
    );
1587
    lockSettings();
1588
    updateDirAndFilesPermissions();
1589
    executeLexikKeyPair($kernel);
1590
1591
    createExtraConfigFile();
1592
}
1593
1594
/**
1595
 * Update settings based on installation profile defined in a JSON file.
1596
 *
1597
 * @param string $installationProfile The name of the JSON file in main/install/profiles/ folder
1598
 *
1599
 * @return bool false on failure (no bad consequences anyway, just ignoring profile)
1600
 */
1601
function installProfileSettings($installationProfile = '')
1602
{
1603
    error_log('installProfileSettings');
1604
    if (empty($installationProfile)) {
1605
        return false;
1606
    }
1607
    $jsonPath = api_get_path(SYS_PATH).'main/install/profiles/'.$installationProfile.'.json';
1608
    // Make sure the path to the profile is not hacked
1609
    if (!Security::check_abs_path($jsonPath, api_get_path(SYS_PATH).'main/install/profiles/')) {
1610
        return false;
1611
    }
1612
    if (!is_file($jsonPath)) {
1613
        return false;
1614
    }
1615
    if (!is_readable($jsonPath)) {
1616
        return false;
1617
    }
1618
    if (!function_exists('json_decode')) {
1619
        // The php-json extension is not available. Ignore profile.
1620
        return false;
1621
    }
1622
    $json = file_get_contents($jsonPath);
1623
    $params = json_decode($json);
1624
    if (false === $params or null === $params) {
1625
        return false;
1626
    }
1627
    $settings = $params->params;
1628
    if (!empty($params->parent)) {
1629
        installProfileSettings($params->parent);
1630
    }
1631
1632
    $tblSettings = Database::get_main_table(TABLE_MAIN_SETTINGS);
1633
1634
    foreach ($settings as $id => $param) {
1635
        $conditions = ['variable = ? ' => $param->variable];
1636
1637
        if (!empty($param->subkey)) {
1638
            $conditions['AND subkey = ? '] = $param->subkey;
1639
        }
1640
1641
        Database::update(
1642
            $tblSettings,
1643
            ['selected_value' => $param->selected_value],
1644
            $conditions
1645
        );
1646
    }
1647
1648
    return true;
1649
}
1650
1651
/**
1652
 * Quick function to remove a directory with its subdirectories.
1653
 *
1654
 * @param $dir
1655
 */
1656
function rrmdir($dir)
1657
{
1658
    if (is_dir($dir)) {
1659
        $objects = scandir($dir);
1660
        foreach ($objects as $object) {
1661
            if ('.' != $object && '..' != $object) {
1662
                if ('dir' == filetype($dir.'/'.$object)) {
1663
                    @rrmdir($dir.'/'.$object);
1664
                } else {
1665
                    @unlink($dir.'/'.$object);
1666
                }
1667
            }
1668
        }
1669
        reset($objects);
1670
        rmdir($dir);
1671
    }
1672
}
1673
1674
/**
1675
 * Control the different steps of the migration through a big switch.
1676
 *
1677
 * @param string        $fromVersion
1678
 * @param EntityManager $manager
1679
 * @param bool          $processFiles
1680
 *
1681
 * @return bool Always returns true except if the process is broken
1682
 */
1683
function migrateSwitch($fromVersion, $manager, $processFiles = true)
1684
{
1685
    error_log('-----------------------------------------');
1686
    error_log('Starting migration process from '.$fromVersion.' ('.date('Y-m-d H:i:s').')');
1687
    //echo '<a class="btn btn--secondary" href="javascript:void(0)" id="details_button">'.get_lang('Details').'</a><br />';
1688
    //echo '<div id="details" style="display:none">';
1689
    $connection = $manager->getConnection();
1690
1691
    switch ($fromVersion) {
1692
        case '1.11.0':
1693
        case '1.11.1':
1694
        case '1.11.2':
1695
        case '1.11.4':
1696
        case '1.11.6':
1697
        case '1.11.8':
1698
        case '1.11.10':
1699
        case '1.11.12':
1700
        case '1.11.14':
1701
        case '1.11.16':
1702
            $start = time();
1703
            // Migrate using the migration files located in:
1704
            // /srv/http/chamilo2/src/CoreBundle/Migrations/Schema/V200
1705
            $result = migrate($manager);
1706
            error_log('-----------------------------------------');
1707
1708
            if ($result) {
1709
                error_log('Migrations files were executed ('.date('Y-m-d H:i:s').')');
1710
                $sql = "UPDATE settings SET selected_value = '2.0.0'
1711
                        WHERE variable = 'chamilo_database_version'";
1712
                $connection->executeQuery($sql);
1713
                if ($processFiles) {
1714
                    error_log('Update config files');
1715
                    include __DIR__.'/update-files-1.11.0-2.0.0.inc.php';
1716
                    // Only updates the configuration.inc.php with the new version
1717
                    //include __DIR__.'/update-configuration.inc.php';
1718
                }
1719
                $finish = time();
1720
                $total = round(($finish - $start) / 60);
1721
                error_log('Database migration finished:  ('.date('Y-m-d H:i:s').') took '.$total.' minutes');
1722
            } else {
1723
                error_log('There was an error during running migrations. Check error.log');
1724
                exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1725
            }
1726
            break;
1727
        default:
1728
            break;
1729
    }
1730
1731
    //echo '</div>';
1732
1733
    return true;
1734
}
1735
1736
/**
1737
 * @return string
1738
 */
1739
function generateRandomToken()
1740
{
1741
    return hash('sha1', uniqid(mt_rand(), true));
1742
}
1743
1744
/**
1745
 * This function checks if the given file can be created or overwritten.
1746
 *
1747
 * @param string $file Full path to a file
1748
 */
1749
function checkCanCreateFile(string $file): bool
1750
{
1751
    if (file_exists($file)) {
1752
        return is_writable($file);
1753
    }
1754
1755
    $write = @file_put_contents($file, '');
1756
1757
    if (false !== $write) {
1758
        unlink($file);
1759
1760
        return true;
1761
    }
1762
1763
    return false;
1764
}
1765
1766
/**
1767
 * Checks if the update option is available.
1768
 *
1769
 * This function checks the APP_INSTALLED environment variable to determine if the application is already installed.
1770
 * If the APP_INSTALLED variable is set to '1', it indicates that an update is available.
1771
 *
1772
 * @return bool True if the application is already installed (APP_INSTALLED='1'), otherwise false.
1773
 */
1774
function isUpdateAvailable(): bool
1775
{
1776
    $dotenv = new Dotenv();
1777
    $envFile = api_get_path(SYMFONY_SYS_PATH) . '.env';
1778
    $dotenv->loadEnv($envFile);
1779
1780
    // Check if APP_INSTALLED is set and equals '1'
1781
    if (isset($_ENV['APP_INSTALLED']) && $_ENV['APP_INSTALLED'] === '1') {
1782
        return true;
1783
    }
1784
1785
    // If APP_INSTALLED is not found or not set to '1', assume the application is not installed
1786
    return false;
1787
}
1788
1789
/**
1790
 * Check the current migration status.
1791
 *
1792
 * This function calculates the progress of the database migration by comparing the number of executed migrations
1793
 * with the total number of migration files available in the system. It also retrieves the latest executed migration version.
1794
 *
1795
 * @return array {
1796
 *     An array containing the following keys:
1797
 *
1798
 *     @type int    $progress_percentage The percentage of migrations that have been executed.
1799
 *     @type string $current_migration   The version of the last executed migration, or null if no migrations have been executed.
1800
 * }
1801
 */
1802
function checkMigrationStatus(): array
1803
{
1804
    Database::setManager(initializeEntityManager());
1805
    $manager = Database::getManager();
1806
    $connection = $manager->getConnection();
1807
1808
    $migrationFiles = glob(__DIR__ . '/../../../src/CoreBundle/Migrations/Schema/V200/Version*.php');
1809
    $totalMigrations = count($migrationFiles);
1810
1811
    $executedMigrations = $connection->createQueryBuilder()
1812
        ->select('COUNT(*) as count')
1813
        ->from('version')
1814
        ->execute()
1815
        ->fetchOne();
1816
1817
    $progress_percentage = 0;
1818
    if ($totalMigrations > 0) {
1819
        $progress_percentage = ($executedMigrations / $totalMigrations) * 100;
1820
    }
1821
1822
    $current_migration = $connection->createQueryBuilder()
1823
        ->select('version')
1824
        ->from('version')
1825
        ->orderBy('executed_at', 'DESC')
1826
        ->setMaxResults(1)
1827
        ->execute()
1828
        ->fetchOne();
1829
1830
    return [
1831
        'progress_percentage' => ceil($progress_percentage),
1832
        'current_migration' => $current_migration,
1833
    ];
1834
}
1835
1836
/**
1837
 * Initializes the EntityManager by loading environment variables and connecting to the database.
1838
 *
1839
 * @return EntityManager The initialized EntityManager
1840
 */
1841
function initializeEntityManager(): EntityManager
1842
{
1843
    $dotenv = new Dotenv();
1844
    $envFile = api_get_path(SYMFONY_SYS_PATH) . '.env';
1845
    $dotenv->loadEnv($envFile);
1846
1847
    connectToDatabase(
1848
        $_ENV['DATABASE_HOST'],
1849
        $_ENV['DATABASE_USER'],
1850
        $_ENV['DATABASE_PASSWORD'],
1851
        $_ENV['DATABASE_NAME'],
1852
        $_ENV['DATABASE_PORT']
1853
    );
1854
1855
    $manager = Database::getManager();
1856
1857
    return $manager;
1858
}
1859
1860
/**
1861
 * Checks if the version table in the database is valid.
1862
 *
1863
 * @param Connection $connection The database connection
1864
 *
1865
 * @return bool True if the version table is valid, false otherwise
1866
 */
1867
function isVersionTableValid($connection): bool
1868
{
1869
    $schema = $connection->createSchemaManager();
1870
    if ($schema->tablesExist('version')) {
1871
        $columns = $schema->listTableColumns('version');
1872
1873
        $requiredColumns = ['version', 'executed_at', 'execution_time'];
1874
        foreach ($requiredColumns as $column) {
1875
            if (!isset($columns[$column])) {
1876
                return false;
1877
            }
1878
        }
1879
1880
        $query = $connection->createQueryBuilder()
1881
            ->select('*')
1882
            ->from('version')
1883
            ->orderBy('executed_at', 'DESC')
1884
            ->setMaxResults(1);
1885
        $result = $query->execute()->fetchAll();
1886
1887
        if (!empty($result)) {
1888
            $latestMigrationDate = new DateTime($result[0]['executed_at']);
1889
            $now = new DateTime();
1890
1891
            if ($latestMigrationDate->diff($now)->days < 1) {
1892
                return true;
1893
            }
1894
        }
1895
    }
1896
1897
    return false;
1898
}
1899
1900
/**
1901
 * Retrieves the last executed migration version from the database.
1902
 *
1903
 * @param Connection $connection The database connection
1904
 *
1905
 * @return string The last executed migration version
1906
 */
1907
function getLastExecutedMigration(Connection $connection): string
1908
{
1909
    $query = $connection->createQueryBuilder()
1910
        ->select('version')
1911
        ->from('version')
1912
        ->orderBy('executed_at', 'DESC')
1913
        ->setMaxResults(1);
1914
    $result = $query->execute()->fetchAssociative();
1915
    return $result['version'] ?? '';
1916
}
1917
1918
1919
/**
1920
 * Executes the database migration and returns the status.
1921
 *
1922
 * @return array The result status of the migration
1923
 */
1924
function executeMigration(): array
1925
{
1926
    $resultStatus = [
1927
        'status' => false,
1928
        'message' => 'Error executing migration.',
1929
        'progress_percentage' => 0,
1930
        'current_migration' => '',
1931
    ];
1932
1933
    Database::setManager(initializeEntityManager());
1934
    $manager = Database::getManager();
1935
    $connection = $manager->getConnection();
1936
1937
    try {
1938
        $config = new PhpFile(api_get_path(SYS_CODE_PATH) . 'install/migrations.php');
1939
        $dependency = DependencyFactory::fromConnection($config, new ExistingConnection($connection));
1940
1941
        if (!isVersionTableValid($connection)) {
1942
            $schema = $connection->createSchemaManager();
1943
            $schema->dropTable('version');
1944
        }
1945
1946
        $dependency->getMetadataStorage()->ensureInitialized();
1947
1948
        $env = $_SERVER['APP_ENV'] ?? 'dev';
1949
        $kernel = new Chamilo\Kernel($env, false);
1950
        $kernel->boot();
1951
1952
        $application = new Application($kernel);
1953
        $application->setAutoExit(false);
1954
1955
        $input = new ArrayInput([
1956
            'command' => 'doctrine:migrations:migrate',
1957
            '--no-interaction' => true,
1958
        ]);
1959
1960
        $output = new BufferedOutput();
1961
        $application->run($input, $output);
1962
1963
        $result = $output->fetch();
1964
1965
        createExtraConfigFile();
1966
1967
        if (strpos($result, '[OK] Successfully migrated to version') !== false) {
1968
            $resultStatus['status'] = true;
1969
            $resultStatus['message'] = 'Migration completed successfully.';
1970
            $resultStatus['progress_percentage'] = 100;
1971
        } else {
1972
            $resultStatus['message'] = 'Migration completed with errors.';
1973
            $resultStatus['progress_percentage'] = 0;
1974
        }
1975
1976
        $resultStatus['current_migration'] = getLastExecutedMigration($connection);
1977
    } catch (Exception $e) {
1978
        $resultStatus['current_migration'] = getLastExecutedMigration($connection);
1979
        $resultStatus['message'] = 'Migration failed: ' . $e->getMessage();
1980
    }
1981
1982
    return $resultStatus;
1983
}
1984
1985
/**
1986
 * @throws Exception
1987
 */
1988
function executeLexikKeyPair(\Chamilo\Kernel $kernel): void
1989
{
1990
    $application = new Application($kernel);
1991
    $application->setAutoExit(false);
1992
1993
    $input = new ArrayInput([
1994
        'command' => 'lexik:jwt:generate-keypair',
1995
    ]);
1996
1997
    $output = new NullOutput();
1998
1999
    $application->run($input, $output);
2000
}
2001
2002
function createExtraConfigFile(): void {
2003
    $files = [
2004
        'authentication',
2005
        'settings_overrides',
2006
        'plugin',
2007
    ];
2008
2009
    $sysPath = api_get_path(SYMFONY_SYS_PATH);
2010
2011
    foreach ($files as $file) {
2012
        $finalFilename = $sysPath."config/$file.yaml";
2013
2014
        if (!file_exists($finalFilename)) {
2015
            $distFilename = $sysPath."config/$file.dist.yaml";
2016
2017
            $contents = file_get_contents($distFilename);
2018
2019
            file_put_contents($finalFilename, $contents);
2020
        }
2021
    }
2022
}
2023