Passed
Pull Request — master (#7011)
by
unknown
09:02
created

Diagnoser::get_chamilo_data()   C

Complexity

Conditions 9
Paths 72

Size

Total Lines 111
Code Lines 82

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 82
c 1
b 0
f 0
nc 72
nop 0
dl 0
loc 111
rs 6.8371

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Enums\ActionIcon;
6
use Chamilo\CoreBundle\Enums\ObjectIcon;
7
use Chamilo\CoreBundle\Enums\StateIcon;
8
9
/**
10
 * Class Diagnoser
11
 * Generates diagnostic information about the system.
12
 *
13
 * Notes:
14
 * - Code comments are in English (as requested).
15
 * - Adjusted to current path constants provided by the platform.
16
 * - Database section is DBAL-3 friendly (no getHost()).
17
 * - Courses space section prefers DB sum (resource_file) with safe fallbacks.
18
 */
19
class Diagnoser
20
{
21
    public const STATUS_OK = 1;
22
    public const STATUS_WARNING = 2;
23
    public const STATUS_ERROR = 3;
24
    public const STATUS_INFORMATION = 4;
25
26
    public function __construct()
27
    {
28
    }
29
30
    /**
31
     * Render diagnostics UI with Tailwind (no Bootstrap).
32
     * Drop-in replacement for show_html().
33
     */
34
    public function show_html()
35
    {
36
        // Section registry (label + short info)
37
        $sections = [
38
            'chamilo' => [
39
                'label' => 'Chamilo',
40
                'info'  => 'State of Chamilo requirements',
41
                'icon'  => 'mdi-cog-outline',
42
            ],
43
            'php' => [
44
                'label' => 'PHP',
45
                'info'  => 'State of PHP settings on the server',
46
                'icon'  => 'mdi-language-php',
47
            ],
48
            'database' => [
49
                'label' => 'Database',
50
                'info'  => 'Database server configuration and metadata',
51
                'icon'  => 'mdi-database',
52
            ],
53
            'webserver' => [
54
                'label' => get_lang('Web server'),
55
                'info'  => 'Information about your webserver configuration',
56
                'icon'  => 'mdi-server',
57
            ],
58
            'paths' => [
59
                'label' => 'Paths',
60
                'info'  => 'api_get_path() constants resolved on this portal',
61
                'icon'  => 'mdi-folder-outline',
62
            ],
63
            'courses_space' => [
64
                'label' => 'Courses space',
65
                'info'  => 'Disk usage per course vs disk quota',
66
                'icon'  => 'mdi-folder-cog-outline',
67
            ],
68
        ];
69
70
        $current = isset($_GET['section']) ? trim((string) $_GET['section']) : '';
71
        if (!array_key_exists($current, $sections)) {
72
            $current = 'chamilo';
73
        }
74
75
        // Header
76
        echo $this->tw_header(
77
            title: 'System status',
78
            subtitle: $sections[$current]['info']
79
        );
80
81
        // Icon cards navigation
82
        echo $this->tw_nav_cards($sections, $current);
83
84
        // Section notice
85
        echo $this->tw_notice($sections[$current]['info']);
86
87
        // Fetch data
88
        $method = 'get_'.$current.'_data';
89
        $data   = call_user_func([$this, $method]);
90
91
        // Render per-section
92
        if ('paths' === $current) {
93
            // $data = ['headers' => [...], 'data' => [CONST => value, ...]]
94
            $headers = $data['headers'] ?? ['Path', 'constant'];
95
            $rows    = [];
96
            foreach (($data['data'] ?? []) as $const => $value) {
97
                $rows[] = [$value, $const];
98
            }
99
            echo $this->tw_table(headers: $headers, rows: $rows, dense: false);
100
        } elseif ('courses_space' === $current) {
101
            // $data = list of rows: [homeLink, code, sizeMB, quotaMB, editLink, lastVisit, dirAbs]
102
            $headers = [
103
                '', get_lang('Course code'), 'Space used on disk (MB)',
104
                'Set max course space (MB)', get_lang('Edit'), get_lang('Latest visit'),
105
                get_lang('Current folder'),
106
            ];
107
            echo $this->tw_table(headers: $headers, rows: $data, dense: true);
108
        } else {
109
            // Generic 6-column dataset from build_setting()
110
            $headers = [
111
                '', get_lang('Section'), get_lang('Setting'),
112
                get_lang('Current'), get_lang('Expected'), get_lang('Comment'),
113
            ];
114
            echo $this->tw_table(headers: $headers, rows: $data, dense: true);
115
        }
116
    }
117
118
    /* ---------- Tailwind view helpers (pure HTML, no Bootstrap) ---------- */
119
120
    /**
121
     * Nice page header.
122
     */
123
    private function tw_header(string $title, string $subtitle): string
124
    {
125
        // Tailwind header with subtle divider
126
        return '
127
<div class="mb-6">
128
  <h1 class="text-2xl font-semibold text-gray-900">'.$this->e($title).'</h1>
129
  <p class="mt-1 text-sm text-gray-600">'.$this->e($subtitle).'</p>
130
  <div class="mt-4 h-px w-full bg-gray-30"></div>
131
</div>';
132
    }
133
134
    /**
135
     * Info/notice card.
136
     */
137
    private function tw_notice(string $text): string
138
    {
139
        // Blue info card
140
        return '
141
<div class="mb-6 rounded-xl border border-blue-200 bg-blue-50 p-4 text-blue-800">
142
  <div class="flex items-start gap-3">
143
    <i class="mdi mdi-information-outline text-xl leading-none"></i>
144
    <p class="text-sm">'.$text.'</p>
145
  </div>
146
</div>';
147
    }
148
149
    /**
150
     * Icon card navigation for sections.
151
     * Highlights current section with ring + bg.
152
     */
153
    private function tw_nav_cards(array $sections, string $current): string
154
    {
155
        $html = '<div class="mb-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3">';
156
        foreach ($sections as $key => $meta) {
157
            $active = $key === $current;
158
            $ring   = $active ? 'ring-2 ring-primary/80 bg-primary/5' : 'ring-1 ring-gray-200 hover:ring-gray-300';
159
            $txt    = $active ? 'text-primary' : 'text-gray-700 group-hover:text-gray-900';
160
            $badge  = $active ? '<span class="ml-auto rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">Active</span>' : '';
161
            $url    = 'system_status.php?section='.$key;
162
163
            $html .= '
164
  <a href="'.$url.'" class="group block rounded-2xl bg-white p-4 shadow-sm hover:shadow-md transition '.$ring.'">
165
    <div class="flex items-center gap-3">
166
      <div class="flex h-12 w-12 items-center justify-center rounded-xl bg-gray-10 group-hover:bg-gray-30">
167
        <i class="mdi '.$this->e($meta['icon']).' text-2xl '.$txt.'"></i>
168
      </div>
169
      <div class="min-w-0">
170
        <div class="flex items-center gap-2">
171
          <h3 class="truncate text-sm font-semibold '.$txt.'">'.$this->e($meta['label']).'</h3>
172
          '.$badge.'
173
        </div>
174
        <p class="mt-0.5 line-clamp-2 text-xs text-gray-500">'.$this->e($meta['info']).'</p>
175
      </div>
176
    </div>
177
  </a>';
178
        }
179
        $html .= '</div>';
180
181
        return $html;
182
    }
183
184
    /**
185
     * Tailwind table renderer.
186
     * - sticky header
187
     * - subtle row separators
188
     * - optional dense mode
189
     */
190
    private function tw_table(array $headers, array $rows, bool $dense = true): string
191
    {
192
        $thPad = $dense ? 'px-3 py-2' : 'px-4 py-3';
193
        $tdPad = $dense ? 'px-3 py-2' : 'px-4 py-3';
194
195
        $html = '
196
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
197
  <div class="overflow-x-auto">
198
    <table class="min-w-full text-sm text-gray-800">
199
      <thead class="bg-gray-20 text-left text-xs font-semibold text-gray-600">
200
        <tr>';
201
202
        foreach ($headers as $h) {
203
            $html .= '<th scope="col" class="sticky top-0 z-10 '.$thPad.'">'.$h.'</th>';
204
        }
205
206
        $html .= '
207
        </tr>
208
      </thead>
209
      <tbody class="divide-y divide-gray-100">';
210
211
        foreach ($rows as $row) {
212
            $html .= '<tr class="hover:bg-gray-20">';
213
            foreach ($row as $cell) {
214
                // Allow HTML for icons/links already generated upstream
215
                $html .= '<td class="'.$tdPad.' align-top">'.(string) $cell.'</td>';
216
            }
217
            $html .= '</tr>';
218
        }
219
220
        $html .= '
221
      </tbody>
222
    </table>
223
  </div>
224
</div>';
225
226
        return $html;
227
    }
228
229
    /**
230
     * Simple HTML escaper for plain strings.
231
     */
232
    private function e(string $v): string
233
    {
234
        return htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
235
    }
236
237
    /**
238
     * Paths section (robust to current constant set).
239
     * Fix: do NOT use $paths[api_get_path(WEB_PATH)] as array key anymore.
240
     *
241
     * @return array{headers:array<string>,data:array<string,string>}
242
     */
243
    public function get_paths_data()
244
    {
245
        // Keep this list in sync with the provided platform constants.
246
        $constNames = [
247
            // Relative helpers
248
            'REL_CODE_PATH',
249
            'REL_COURSE_PATH',
250
            'REL_HOME_PATH',
251
252
            // Registered path types for api_get_path()
253
            'WEB_PATH',
254
            'SYS_PATH',
255
            'SYMFONY_SYS_PATH',
256
257
            'REL_PATH',
258
            'WEB_COURSE_PATH',
259
            'WEB_CODE_PATH',
260
            'SYS_CODE_PATH',
261
            'SYS_LANG_PATH',
262
            'WEB_IMG_PATH',
263
            'WEB_CSS_PATH',
264
            'WEB_PUBLIC_PATH',
265
            'SYS_CSS_PATH',
266
            'SYS_PLUGIN_PATH',
267
            'WEB_PLUGIN_PATH',
268
            'WEB_PLUGIN_ASSET_PATH',
269
            'SYS_ARCHIVE_PATH',
270
            'WEB_ARCHIVE_PATH',
271
            'LIBRARY_PATH',
272
            'CONFIGURATION_PATH',
273
            'WEB_LIBRARY_PATH',
274
            'WEB_LIBRARY_JS_PATH',
275
            'WEB_AJAX_PATH',
276
            'SYS_TEST_PATH',
277
            'SYS_TEMPLATE_PATH',
278
            'SYS_PUBLIC_PATH',
279
            'SYS_FONTS_PATH',
280
        ];
281
282
        $list = [];
283
        foreach ($constNames as $name) {
284
            if (defined($name)) {
285
                $value = api_get_path(constant($name));
286
                if ($value !== false && $value !== null && $value !== '') {
287
                    // Map CONSTANT => resolved value
288
                    $list[$name] = $value;
289
                }
290
            }
291
        }
292
293
        // Sort by resolved path for readability, preserving keys (constants)
294
        asort($list);
295
296
        return [
297
            'headers' => ['Path', 'constant'],
298
            'data' => $list,
299
        ];
300
    }
301
302
    /**
303
     * Chamilo requirements snapshot.
304
     *
305
     * @return array<int,array>
306
     */
307
    public function get_chamilo_data()
308
    {
309
        $array = [];
310
        $writable_folders = [
311
            api_get_path(SYS_ARCHIVE_PATH).'cache',
312
            api_get_path(SYS_PATH).'upload/users/',
313
        ];
314
        foreach ($writable_folders as $folder) {
315
            $writable = is_writable($folder);
316
            $status = $writable ? self::STATUS_OK : self::STATUS_ERROR;
317
            $array[] = $this->build_setting(
318
                $status,
319
                '[FILES]',
320
                get_lang('Is writable').': '.$folder,
321
                'http://be2.php.net/manual/en/function.is-writable.php',
322
                $writable,
323
                1,
324
                'yes_no',
325
                get_lang('The directory must be writable by the web server')
326
            );
327
        }
328
329
        $exists = file_exists(api_get_path(SYS_CODE_PATH).'install');
330
        $status = $exists ? self::STATUS_WARNING : self::STATUS_OK;
331
        $array[] = $this->build_setting(
332
            $status,
333
            '[FILES]',
334
            get_lang('The directory exists').': /install',
335
            'http://be2.php.net/file_exists',
336
            $exists,
337
            0,
338
            'yes_no',
339
            get_lang('The directory should be removed (it is no longer necessary)')
340
        );
341
342
        $app_version = api_get_setting('platform.chamilo_database_version');
343
        $array[] = $this->build_setting(
344
            self::STATUS_INFORMATION,
345
            '[DB]',
346
            'chamilo_database_version',
347
            '#',
348
            $app_version,
349
            0,
350
            null,
351
            'Chamilo DB version'
352
        );
353
354
        $access_url_id = api_get_current_access_url_id();
355
356
        if (1 === $access_url_id) {
357
            $size = '-';
358
            $message2 = '';
359
360
            if (api_is_windows_os()) {
361
                $message2 .= get_lang('The space used on disk cannot be measured properly on Windows-based systems.');
362
            } else {
363
                $dir = api_get_path(SYS_PATH);
364
                $du = exec('du -sh '.escapeshellarg($dir), $err);
365
                if (strpos($du, "\t") !== false) {
366
                    list($size, $none) = explode("\t", $du, 2);
367
                    unset($none);
368
                }
369
370
                $limit = get_hosting_limit($access_url_id, 'disk_space');
371
                if ($limit === null) {
372
                    $limit = 0;
373
                }
374
375
                $message2 .= sprintf(get_lang('Total space used by portal %s limit is %s MB'), $size, $limit);
376
            }
377
378
            $array[] = $this->build_setting(
379
                self::STATUS_OK,
380
                '[FILES]',
381
                'hosting_limit_disk_space',
382
                '#',
383
                $size,
384
                0,
385
                null,
386
                $message2
387
            );
388
        }
389
390
        $new_version = '-';
391
        $new_version_status = '';
392
        $file = api_get_path(SYS_CODE_PATH).'install/version.php';
393
        if (is_file($file)) {
394
            @include $file;
395
        }
396
        $array[] = $this->build_setting(
397
            self::STATUS_INFORMATION,
398
            '[CONFIG]',
399
            get_lang('Version from the version file'),
400
            '#',
401
            $new_version.' '.$new_version_status,
402
            '-',
403
            null,
404
            get_lang('The version from the version.php file is updated with each version but only available if the main/install/ directory is present.')
405
        );
406
        $array[] = $this->build_setting(
407
            self::STATUS_INFORMATION,
408
            '[CONFIG]',
409
            get_lang('Version from the config file'),
410
            '#',
411
            api_get_configuration_value('system_version'),
412
            $new_version,
413
            null,
414
            get_lang('The version from the main configuration file shows on the main administration page, but has to be changed manually on upgrade.')
415
        );
416
417
        return $array;
418
    }
419
420
    /**
421
     * PHP settings snapshot.
422
     *
423
     * @return array<int,array>
424
     */
425
    public function get_php_data()
426
    {
427
        $array = [];
428
429
        $version = phpversion();
430
        $status = $version > REQUIRED_PHP_VERSION ? self::STATUS_OK : self::STATUS_ERROR;
431
        $array[] = $this->build_setting(
432
            $status,
433
            '[PHP]',
434
            'phpversion()',
435
            'https://php.net/manual/en/function.phpversion.php',
436
            phpversion(),
437
            '>= '.REQUIRED_PHP_VERSION,
438
            null,
439
            get_lang('PHP version')
440
        );
441
442
        $setting = ini_get('output_buffering');
443
        $req_setting = 1;
444
        $status = $setting >= $req_setting ? self::STATUS_OK : self::STATUS_ERROR;
445
        $array[] = $this->build_setting(
446
            $status,
447
            '[INI]',
448
            'output_buffering',
449
            'https://php.net/manual/en/outcontrol.configuration.php#ini.output-buffering',
450
            $setting,
451
            $req_setting,
452
            'on_off',
453
            get_lang('Output buffering setting is "On" for being enabled or "Off" for being disabled. This setting also may be enabled through an integer value (4096 for example) which is the output buffer size.')
454
        );
455
456
        $setting = ini_get('file_uploads');
457
        $req_setting = 1;
458
        $status = $setting == $req_setting ? self::STATUS_OK : self::STATUS_ERROR;
459
        $array[] = $this->build_setting(
460
            $status,
461
            '[INI]',
462
            'file_uploads',
463
            'https://php.net/manual/en/ini.core.php#ini.file-uploads',
464
            $setting,
465
            $req_setting,
466
            'on_off',
467
            get_lang('File uploads indicate whether file uploads are authorized at all')
468
        );
469
470
        $setting = ini_get('magic_quotes_runtime');
471
        $req_setting = 0;
472
        $status = $setting == $req_setting ? self::STATUS_OK : self::STATUS_ERROR;
473
        $array[] = $this->build_setting(
474
            $status,
475
            '[INI]',
476
            'magic_quotes_runtime',
477
            'https://php.net/manual/en/ini.core.php#ini.magic-quotes-runtime',
478
            $setting,
479
            $req_setting,
480
            'on_off',
481
            get_lang('This is a highly unrecommended feature which converts values returned by all functions that returned external values to slash-escaped values. This feature should *not* be enabled.')
482
        );
483
484
        $setting = ini_get('safe_mode');
485
        $req_setting = 0;
486
        $status = $setting == $req_setting ? self::STATUS_OK : self::STATUS_WARNING;
487
        $array[] = $this->build_setting(
488
            $status,
489
            '[INI]',
490
            'safe_mode',
491
            'https://php.net/manual/en/ini.core.php#ini.safe-mode',
492
            $setting,
493
            $req_setting,
494
            'on_off',
495
            get_lang('Safe mode is a deprecated PHP feature which (badly) limits the access of PHP scripts to other resources. It is recommended to leave it off.')
496
        );
497
498
        $setting = ini_get('register_globals');
499
        $req_setting = 0;
500
        $status = $setting == $req_setting ? self::STATUS_OK : self::STATUS_ERROR;
501
        $array[] = $this->build_setting(
502
            $status,
503
            '[INI]',
504
            'register_globals',
505
            'https://php.net/manual/en/ini.core.php#ini.register-globals',
506
            $setting,
507
            $req_setting,
508
            'on_off',
509
            get_lang('Whether to use the register globals feature or not. Using it represents potential security risks with this software.')
510
        );
511
512
        $setting = ini_get('short_open_tag');
513
        $req_setting = 0;
514
        $status = $setting == $req_setting ? self::STATUS_OK : self::STATUS_WARNING;
515
        $array[] = $this->build_setting(
516
            $status,
517
            '[INI]',
518
            'short_open_tag',
519
            'https://php.net/manual/en/ini.core.php#ini.short-open-tag',
520
            $setting,
521
            $req_setting,
522
            'on_off',
523
            get_lang('Whether to allow for short open tags to be used or not. This feature should not be used.')
524
        );
525
526
        $setting = ini_get('magic_quotes_gpc');
527
        $req_setting = 0;
528
        $status = $setting == $req_setting ? self::STATUS_OK : self::STATUS_ERROR;
529
        $array[] = $this->build_setting(
530
            $status,
531
            '[INI]',
532
            'magic_quotes_gpc',
533
            'https://php.net/manual/en/ini.core.php#ini.magic_quotes_gpc',
534
            $setting,
535
            $req_setting,
536
            'on_off',
537
            get_lang('Whether to automatically escape values from GET, POST and COOKIES arrays. A similar feature is provided for the required data inside this software, so using it provokes double slash-escaping of values.')
538
        );
539
540
        $setting = ini_get('display_errors');
541
        $req_setting = 0;
542
        $status = $setting == $req_setting ? self::STATUS_OK : self::STATUS_WARNING;
543
        $array[] = $this->build_setting(
544
            $status,
545
            '[INI]',
546
            'display_errors',
547
            'https://php.net/manual/en/ini.core.php#ini.display_errors',
548
            $setting,
549
            $req_setting,
550
            'on_off',
551
            get_lang('Show errors on screen. Turn this on on development servers, off on production servers.')
552
        );
553
554
        $setting = ini_get('default_charset');
555
        if ('' == $setting) {
556
            $setting = null;
557
        }
558
        $req_setting = 'UTF-8';
559
        $status = $setting == $req_setting ? self::STATUS_OK : self::STATUS_ERROR;
560
        $array[] = $this->build_setting(
561
            $status,
562
            '[INI]',
563
            'default_charset',
564
            'https://php.net/manual/en/ini.core.php#ini.default-charset',
565
            $setting,
566
            $req_setting,
567
            null,
568
            get_lang('The default character set to be sent when returning pages')
569
        );
570
571
        $setting = ini_get('max_execution_time');
572
        $req_setting = '300 ('.get_lang('minimum').')';
573
        $status = $setting >= 300 ? self::STATUS_OK : self::STATUS_WARNING;
574
        $array[] = $this->build_setting(
575
            $status,
576
            '[INI]',
577
            'max_execution_time',
578
            'https://php.net/manual/en/ini.core.php#ini.max-execution-time',
579
            $setting,
580
            $req_setting,
581
            null,
582
            get_lang('Maximum time a script can take to execute. If using more than that, the script is abandoned to avoid slowing down other users.')
583
        );
584
585
        $setting = ini_get('max_input_time');
586
        $req_setting = '300 ('.get_lang('minimum').')';
587
        $status = $setting >= 300 ? self::STATUS_OK : self::STATUS_WARNING;
588
        $array[] = $this->build_setting(
589
            $status,
590
            '[INI]',
591
            'max_input_time',
592
            'https://php.net/manual/en/ini.core.php#ini.max-input-time',
593
            $setting,
594
            $req_setting,
595
            null,
596
            get_lang('The maximum time allowed for a form to be processed by the server. If it takes longer, the process is abandonned and a blank page is returned.')
597
        );
598
599
        $setting = ini_get('memory_limit');
600
        $req_setting = '>= '.REQUIRED_MIN_MEMORY_LIMIT.'M';
601
        $status = self::STATUS_ERROR;
602
        if ((float) $setting >= REQUIRED_MIN_MEMORY_LIMIT) {
603
            $status = self::STATUS_OK;
604
        }
605
        $array[] = $this->build_setting(
606
            $status,
607
            '[INI]',
608
            'memory_limit',
609
            'https://php.net/manual/en/ini.core.php#ini.memory-limit',
610
            $setting,
611
            $req_setting,
612
            null,
613
            get_lang('Maximum memory limit for one single script run. If the memory needed is higher, the process will stop to avoid consuming all the server\'s available memory and thus slowing down other users.')
614
        );
615
616
        $setting = ini_get('post_max_size');
617
        $req_setting = '>= '.REQUIRED_MIN_POST_MAX_SIZE.'M';
618
        $status = self::STATUS_ERROR;
619
        if ((float) $setting >= REQUIRED_MIN_POST_MAX_SIZE) {
620
            $status = self::STATUS_OK;
621
        }
622
        $array[] = $this->build_setting(
623
            $status,
624
            '[INI]',
625
            'post_max_size',
626
            'https://php.net/manual/en/ini.core.php#ini.post-max-size',
627
            $setting,
628
            $req_setting,
629
            null,
630
            get_lang('This is the maximum size of uploads through forms using the POST method (i.e. classical file upload forms)')
631
        );
632
633
        $setting = ini_get('upload_max_filesize');
634
        $req_setting = '>= '.REQUIRED_MIN_UPLOAD_MAX_FILESIZE.'M';
635
        $status = self::STATUS_ERROR;
636
        if ((float) $setting >= REQUIRED_MIN_UPLOAD_MAX_FILESIZE) {
637
            $status = self::STATUS_OK;
638
        }
639
        $array[] = $this->build_setting(
640
            $status,
641
            '[INI]',
642
            'upload_max_filesize',
643
            'https://php.net/manual/en/ini.core.php#ini.upload_max_filesize',
644
            $setting,
645
            $req_setting,
646
            null,
647
            get_lang('Maximum volume of an uploaded file. This setting should, most of the time, be matched with the post_max_size variable.')
648
        );
649
650
        $setting = ini_get('upload_tmp_dir');
651
        $status = self::STATUS_OK;
652
        $array[] = $this->build_setting(
653
            $status,
654
            '[INI]',
655
            'upload_tmp_dir',
656
            'https://php.net/manual/en/ini.core.php#ini.upload_tmp_dir',
657
            $setting,
658
            '',
659
            null,
660
            get_lang('The temporary upload directory is a space on the server where files are uploaded before being filtered and treated by PHP.')
661
        );
662
663
        $setting = ini_get('variables_order');
664
        $req_setting = 'GPCS';
665
        $status = $setting == $req_setting ? self::STATUS_OK : self::STATUS_ERROR;
666
        $array[] = $this->build_setting(
667
            $status,
668
            '[INI]',
669
            'variables_order',
670
            'https://php.net/manual/en/ini.core.php#ini.variables-order',
671
            $setting,
672
            $req_setting,
673
            null,
674
            get_lang('The order of precedence of Environment, GET, POST, COOKIES and SESSION variables')
675
        );
676
677
        $setting = ini_get('session.gc_maxlifetime');
678
        $req_setting = '4320';
679
        $status = $setting == $req_setting ? self::STATUS_OK : self::STATUS_WARNING;
680
        $array[] = $this->build_setting(
681
            $status,
682
            '[SESSION]',
683
            'session.gc_maxlifetime',
684
            'https://php.net/manual/en/ini.core.php#session.gc-maxlifetime',
685
            $setting,
686
            $req_setting,
687
            null,
688
            get_lang('The session garbage collector maximum lifetime indicates which maximum time is given between two runs of the garbage collector.')
689
        );
690
691
        $setting = api_check_browscap() ? true : false;
692
        $req_setting = true;
693
        $status = $setting == $req_setting ? self::STATUS_OK : self::STATUS_WARNING;
694
        $array[] = $this->build_setting(
695
            $status,
696
            '[INI]',
697
            'browscap',
698
            'https://php.net/manual/en/misc.configuration.php#ini.browscap',
699
            $setting,
700
            $req_setting,
701
            'on_off',
702
            get_lang('Browscap loading browscap.ini file that contains a large amount of data on the browser and its capabilities, so it can be used by the function get_browser () PHP')
703
        );
704
705
        // Extensions
706
        $extensions = [
707
            'curl' => ['link' => 'https://php.net/curl', 'expected' => 1, 'comment' => get_lang('This extension must be loaded.')],
708
            'exif' => ['link' => 'https://www.php.net/exif', 'expected' => 1, 'comment' => get_lang('This extension should be loaded.')],
709
            'fileinfo' => ['link' => 'https://php.net/fileinfo', 'expected' => 1, 'comment' => get_lang('This extension must be loaded.')],
710
            'gd' => ['link' => 'https://php.net/gd', 'expected' => 1, 'comment' => get_lang('This extension must be loaded.')],
711
            'ldap' => ['link' => 'https://php.net/ldap', 'expected' => 1, 'comment' => get_lang('This extension should be loaded.')],
712
            'mbstring' => ['link' => 'https://www.php.net/mbstring', 'expected' => 1, 'comment' => get_lang('This extension should be loaded.')],
713
            'pcre' => ['link' => 'https://php.net/pcre', 'expected' => 1, 'comment' => get_lang('This extension must be loaded.')],
714
            'pdo_mysql' => ['link' => 'https://php.net/manual/en/ref.pdo-mysql.php', 'expected' => 1, 'comment' => get_lang('This extension must be loaded.')],
715
            'session' => ['link' => 'https://php.net/session', 'expected' => 1, 'comment' => get_lang('This extension must be loaded.')],
716
            'standard' => ['link' => 'https://php.net/spl', 'expected' => 1, 'comment' => get_lang('This extension must be loaded.')],
717
            'zlib' => ['link' => 'https://php.net/zlib', 'expected' => 1, 'comment' => get_lang('This extension must be loaded.')],
718
            'apcu' => ['link' => 'https://php.net/apcu', 'expected' => 2, 'comment' => get_lang('This extension should be loaded.')],
719
            'bcmath' => ['link' => 'https://php.net/bcmath', 'expected' => 2, 'comment' => get_lang('This extension should be loaded.')],
720
            'OPcache' => ['link' => 'https://php.net/opcache', 'expected' => 2, 'comment' => get_lang('This extension should be loaded.')],
721
            'openssl' => ['link' => 'https://php.net/openssl', 'expected' => 2, 'comment' => get_lang('This extension should be loaded.')],
722
            'xsl' => ['link' => 'https://php.net/xsl', 'expected' => 2, 'comment' => get_lang('This extension should be loaded.')],
723
        ];
724
725
        foreach ($extensions as $extension => $data) {
726
            $url = $data['link'];
727
            $expected_value = $data['expected'];
728
            $comment = $data['comment'];
729
730
            $loaded = extension_loaded($extension);
731
            $status = $loaded ? self::STATUS_OK : self::STATUS_ERROR;
732
            $array[] = $this->build_setting(
733
                $status,
734
                '[EXTENSION]',
735
                get_lang('Extension loaded').': '.$extension,
736
                $url,
737
                $loaded,
738
                $expected_value,
739
                'yes_no_optional',
740
                $comment
741
            );
742
        }
743
744
        return $array;
745
    }
746
747
    /**
748
     * Database diagnostics (DBAL-3 friendly).
749
     * Fix: avoid Connection::getHost(); rely on params + platform.
750
     *
751
     * @return array<int,array>
752
     */
753
    public function get_database_data()
754
    {
755
        $array = [];
756
        $em = Database::getManager();
757
        $connection = $em->getConnection();
758
759
        // Prefer platform name (mysql, postgresql, sqlite, …)
760
        try {
761
            $driver = $connection->getDatabasePlatform()->getName();
762
        } catch (\Throwable $e) {
763
            $driver = (method_exists($connection, 'getDriver') && method_exists($connection->getDriver(), 'getName'))
764
                ? $connection->getDriver()->getName()
765
                : 'unknown';
766
        }
767
768
        $params = method_exists($connection, 'getParams') ? (array) $connection->getParams() : [];
769
        $primary = isset($params['primary']) && is_array($params['primary']) ? $params['primary'] : $params;
770
771
        $host = $primary['host'] ?? ($primary['unix_socket'] ?? 'localhost');
772
        $port = $primary['port'] ?? null;
773
774
        try {
775
            $db = $connection->getDatabase();
776
        } catch (\Throwable $e) {
777
            $db = $primary['dbname'] ?? ($primary['path'] ?? 'unknown');
778
        }
779
780
        $array[] = $this->build_setting(self::STATUS_INFORMATION, '[Database]', 'driver', '', $driver, null, null, get_lang('Driver'));
781
        $array[] = $this->build_setting(self::STATUS_INFORMATION, '[Database]', 'host', '', $host, null, null, get_lang('MySQL server host'));
782
        $array[] = $this->build_setting(self::STATUS_INFORMATION, '[Database]', 'port', '', (string) $port, null, null, get_lang('Port'));
783
        $array[] = $this->build_setting(self::STATUS_INFORMATION, '[Database]', 'Database name', '', $db, null, null, get_lang('Name'));
784
785
        return $array;
786
    }
787
788
    /**
789
     * Webserver snapshot.
790
     *
791
     * @return array<int,array>
792
     */
793
    public function get_webserver_data()
794
    {
795
        $array = [];
796
797
        $array[] = $this->build_setting(
798
            self::STATUS_INFORMATION,
799
            '[SERVER]',
800
            '$_SERVER["SERVER_NAME"]',
801
            'http://be.php.net/reserved.variables.server',
802
            $_SERVER["SERVER_NAME"] ?? '',
803
            null,
804
            null,
805
            get_lang('Server name (as used in your request)')
806
        );
807
        $array[] = $this->build_setting(
808
            self::STATUS_INFORMATION,
809
            '[SERVER]',
810
            '$_SERVER["SERVER_ADDR"]',
811
            'http://be.php.net/reserved.variables.server',
812
            $_SERVER["SERVER_ADDR"] ?? '',
813
            null,
814
            null,
815
            get_lang('Server address')
816
        );
817
        $array[] = $this->build_setting(
818
            self::STATUS_INFORMATION,
819
            '[SERVER]',
820
            '$_SERVER["SERVER_PORT"]',
821
            'http://be.php.net/reserved.variables.server',
822
            $_SERVER["SERVER_PORT"] ?? '',
823
            null,
824
            null,
825
            get_lang('Server port')
826
        );
827
        $array[] = $this->build_setting(
828
            self::STATUS_INFORMATION,
829
            '[SERVER]',
830
            '$_SERVER["SERVER_SOFTWARE"]',
831
            'http://be.php.net/reserved.variables.server',
832
            $_SERVER["SERVER_SOFTWARE"] ?? '',
833
            null,
834
            null,
835
            get_lang('Software running as a web server')
836
        );
837
        $array[] = $this->build_setting(
838
            self::STATUS_INFORMATION,
839
            '[SERVER]',
840
            '$_SERVER["REMOTE_ADDR"]',
841
            'http://be.php.net/reserved.variables.server',
842
            $_SERVER["REMOTE_ADDR"] ?? '',
843
            null,
844
            null,
845
            get_lang('Remote address (your address as received by the server)')
846
        );
847
        $array[] = $this->build_setting(
848
            self::STATUS_INFORMATION,
849
            '[SERVER]',
850
            '$_SERVER["HTTP_USER_AGENT"]',
851
            'http://be.php.net/reserved.variables.server',
852
            $_SERVER["HTTP_USER_AGENT"] ?? '',
853
            null,
854
            null,
855
            get_lang('Your user agent as received by the server')
856
        );
857
        $array[] = $this->build_setting(
858
            self::STATUS_INFORMATION,
859
            '[SERVER]',
860
            '$_SERVER["SERVER_PROTOCOL"]',
861
            'http://be.php.net/reserved.variables.server',
862
            $_SERVER["SERVER_PROTOCOL"] ?? '',
863
            null,
864
            null,
865
            get_lang('Protocol used by this server')
866
        );
867
        $array[] = $this->build_setting(
868
            self::STATUS_INFORMATION,
869
            '[SERVER]',
870
            'php_uname()',
871
            'http://be2.php.net/php_uname',
872
            php_uname(),
873
            null,
874
            null,
875
            get_lang('Information on the system the current server is running on')
876
        );
877
        $array[] = $this->build_setting(
878
            self::STATUS_INFORMATION,
879
            '[SERVER]',
880
            '$_SERVER["HTTP_X_FORWARDED_FOR"]',
881
            'http://be.php.net/reserved.variables.server',
882
            (!empty($_SERVER["HTTP_X_FORWARDED_FOR"]) ? $_SERVER["HTTP_X_FORWARDED_FOR"] : ''),
883
            null,
884
            null,
885
            get_lang('If the server is behind a proxy or firewall (and only in those cases), it might be using the X_FORWARDED_FOR HTTP header to show the remote user IP (yours, in this case).')
886
        );
887
888
        return $array;
889
    }
890
891
    /**
892
     * Return "Courses space" rows using DB sums (no filesystem scan).
893
     * Columns (legacy order):
894
     * [ homeLink, code, usedMB, quotaMB, editLink, last_visit, absPathHint ]
895
     *
896
     * v2 notes:
897
     * - There is no per-course public folder anymore.
898
     * - We compute sizes from ResourceFile (rf.size) linked through ResourceNode/ResourceLink.
899
     * - Use rl.c_id (NOT rl.course_id).
900
     * - We de-duplicate per (course_id, rf.id) to avoid double counting in the same course.
901
     * - We do NOT include Asset (global) files as they are not course-scoped.
902
     */
903
    public function get_courses_space_data()
904
    {
905
        $rows = [];
906
907
        $em   = Database::getManager();
908
        $conn = $em->getConnection();
909
910
        // Aggregate used bytes from ResourceFile (no FS scan).
911
        $sql = <<<SQL
912
        SELECT
913
            c.id,
914
            c.code,
915
            c.disk_quota,
916
            c.last_visit,
917
            COALESCE(SUM(u.size), 0) AS used_bytes
918
        FROM course c
919
        LEFT JOIN (
920
            SELECT DISTINCT
921
                rl.c_id   AS course_id,
922
                rf.id     AS rf_id,
923
                rf.size   AS size
924
            FROM resource_link rl
925
            INNER JOIN resource_node rn ON rn.id = rl.resource_node_id
926
            INNER JOIN resource_file rf ON rf.resource_node_id = rn.id
927
        ) u ON u.course_id = c.id
928
        GROUP BY c.id, c.code, c.disk_quota, c.last_visit
929
        ORDER BY c.last_visit DESC, c.code ASC
930
        LIMIT 1000
931
    SQL;
932
933
        $data = $conn->executeQuery($sql)->fetchAllAssociative();
934
935
        // Icons (no dead links in v2)
936
        $homeIcon = Display::getMdiIcon(ObjectIcon::HOME, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Course homepage'));
937
        $editIcon = Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit'));
938
        $editBase = api_get_path(WEB_CODE_PATH).'admin/course_edit.php?id=';
939
940
        // v2 has no per-course absolute folder; provide a neutral hint.
941
        $storageHint = 'resource storage (v2 via Vich/Flysystem)';
942
943
        // Resolve platform default course quota once (MB)
944
        $defaultQuotaMb = $this->resolveDefaultCourseQuotaMb();
945
946
        foreach ($data as $row) {
947
            // Used bytes -> MB, min 1MB if > 0 to keep legacy semantics
948
            $bytes  = (int) ($row['used_bytes'] ?? 0);
949
            $usedMb = $bytes > 0 ? max(1, (int) ceil($bytes / (1024 * 1024))) : 0;
950
951
            // Quota: per-course override if set (>0) else platform default (MB)
952
            // c.disk_quota is stored in BYTES; convert to MB if >0
953
            $quotaMb = ((int) $row['disk_quota'] > 0)
954
                ? (int) $row['disk_quota']
955
                : $defaultQuotaMb;
956
957
            $homeLink = $homeIcon; // no href by default in v2
958
            $editLink = '<a href="'.$editBase.(int) $row['id'].'">'.$editIcon.'</a>';
959
960
            $rows[] = [
961
                $homeLink,
962
                $row['code'],
963
                $usedMb,
964
                $quotaMb,
965
                $editLink,
966
                $row['last_visit'],
967
                $storageHint,
968
            ];
969
        }
970
971
        return $rows;
972
    }
973
974
    /**
975
     * Resolve the platform's default course quota in MB (robust).
976
     * Tries, in order:
977
     *  1) SettingsManager (v2)
978
     *  2) api_get_setting() (legacy)
979
     *  3) DB table settings (direct)
980
     *  4) DocumentManager::get_course_quota() fallback
981
     */
982
    private function resolveDefaultCourseQuotaMb(): int
983
    {
984
        // 1) v2 SettingsManager (if available)
985
        try {
986
            if (class_exists('Container') && method_exists('Container', 'getSettingsManager')) {
987
                $sm = \Container::getSettingsManager();
988
                if ($sm) {
989
                    $candidates = [
990
                        'course.course_quota',                 // expected v2 key
991
                        'document.default_course_quota',       // legacy-friendly
992
                        'document.default_document_quota',
993
                        'document.default_document_quotum',    // v1 spelling
994
                    ];
995
                    foreach ($candidates as $key) {
996
                        $raw = (string) $sm->getSetting($key);
997
                        if ($raw !== '' && $raw !== '0' && $raw !== null) {
998
                            $mb = $this->parseQuotaRawToMb($raw);
999
                            if ($mb >= 0) {
1000
                                return $mb;
1001
                            }
1002
                        }
1003
                    }
1004
                }
1005
            }
1006
        } catch (\Throwable $e) {
1007
            // Ignore and continue with other strategies
1008
        }
1009
1010
        // 2) api_get_setting() (legacy accessor)
1011
        $candidates = [
1012
            'course.course_quota',
1013
            'document.default_course_quota',
1014
            'document.default_document_quota',
1015
            'document.default_document_quotum',
1016
        ];
1017
        foreach ($candidates as $key) {
1018
            $raw = api_get_setting($key);
1019
            if ($raw !== false && $raw !== null && $raw !== '') {
1020
                $mb = $this->parseQuotaRawToMb((string) $raw);
1021
                if ($mb >= 0) {
1022
                    return $mb;
1023
                }
1024
            }
1025
        }
1026
1027
        // 3) Direct DB read from settings (works on most v1/v2 installs)
1028
        try {
1029
            $em   = Database::getManager();
1030
            $conn = $em->getConnection();
1031
1032
            foreach ($candidates as $key) {
1033
                $val = $conn->fetchOne(
1034
                    'SELECT value FROM settings WHERE variable = ? LIMIT 1',
1035
                    [$key]
1036
                );
1037
                if ($val !== false && $val !== null && $val !== '') {
1038
                    $mb = $this->parseQuotaRawToMb((string) $val);
1039
                    if ($mb >= 0) {
1040
                        return $mb;
1041
                    }
1042
                }
1043
            }
1044
        } catch (\Throwable $e) {
1045
            // Ignore and continue
1046
        }
1047
1048
        // 4) Last resort: ask DocumentManager (may return platform default)
1049
        try {
1050
            if (class_exists('DocumentManager') && method_exists('DocumentManager', 'get_course_quota')) {
1051
                $v = \DocumentManager::get_course_quota(); // usually returns MB
1052
                if (is_numeric($v)) {
1053
                    $mb = $this->parseQuotaRawToMb((string) $v);
1054
                    if ($mb >= 0) {
1055
                        return $mb;
1056
                    }
1057
                }
1058
            }
1059
        } catch (\Throwable $e) {
1060
            // Ignore
1061
        }
1062
1063
        // Nothing found => keep legacy semantics (0 = no explicit default)
1064
        return 0;
1065
    }
1066
1067
    /**
1068
     * Parse a quota raw value into MB.
1069
     * Accepts:
1070
     *  - "500"               -> 500 MB
1071
     *  - "1G", "1GB", "1 g"  -> 1024 MB
1072
     *  - "200M", "200MB"     -> 200 MB
1073
     *  - large integers      -> assumed BYTES, converted to MB
1074
     *  - strings with noise  -> extracts digits & unit heuristically
1075
     */
1076
    private function parseQuotaRawToMb(string $raw): int
1077
    {
1078
        $s = strtolower(trim($raw));
1079
1080
        // Pure integer?
1081
        if (preg_match('/^\d+$/', $s)) {
1082
            $num = (int) $s;
1083
            // Heuristic: if looks like bytes (>= 1MB in bytes), convert to MB.
1084
            return ($num >= 1048576) ? (int) ceil($num / 1048576) : $num;
1085
        }
1086
1087
        // <number><unit> where unit is m/mb or g/gb
1088
        if (preg_match('/^\s*(\d+)\s*([mg])(?:b)?\s*$/i', $s, $m)) {
1089
            $num  = (int) $m[1];
1090
            $unit = strtolower($m[2]);
1091
            return $unit === 'g' ? $num * 1024 : $num;
1092
        }
1093
1094
        // Extract digits for numbers hidden inside strings (e.g. "500 MB", "524288000 bytes", etc.)
1095
        if (preg_match('/(\d+)/', $s, $m)) {
1096
            $num = (int) $m[1];
1097
            return ($num >= 1048576) ? (int) ceil($num / 1048576) : $num;
1098
        }
1099
1100
        // Unknown
1101
        return 0;
1102
    }
1103
1104
    /**
1105
     * Count courses (simple and fast).
1106
     */
1107
    public function get_courses_space_count(): int
1108
    {
1109
        $em   = Database::getManager();
1110
        $conn = $em->getConnection();
1111
1112
        $sql = 'SELECT COUNT(*) AS cnt FROM course';
1113
1114
        return (int) $conn->executeQuery($sql)->fetchOne();
1115
    }
1116
1117
1118
    /**
1119
     * Helper to normalize a diagnostic row.
1120
     *
1121
     * @param int    $status
1122
     * @param string $section
1123
     * @param string $title
1124
     * @param string $url
1125
     * @param mixed  $current_value
1126
     * @param mixed  $expected_value
1127
     * @param string|null $formatter
1128
     * @param string $comment
1129
     *
1130
     * @return array
1131
     */
1132
    public function build_setting(
1133
        $status,
1134
        $section,
1135
        $title,
1136
        $url,
1137
        $current_value,
1138
        $expected_value,
1139
        $formatter,
1140
        $comment
1141
    ) {
1142
        switch ($status) {
1143
            case self::STATUS_OK:
1144
                $img = StateIcon::COMPLETE;
1145
                break;
1146
            case self::STATUS_WARNING:
1147
                $img = StateIcon::WARNING;
1148
                break;
1149
            case self::STATUS_ERROR:
1150
                $img = StateIcon::ERROR;
1151
                break;
1152
            case self::STATUS_INFORMATION:
1153
            default:
1154
                $img = ActionIcon::INFORMATION;
1155
                break;
1156
        }
1157
1158
        $image = Display::getMdiIcon($img, 'ch-tool-icon', null, ICON_SIZE_SMALL, $title);
1159
        $url = $this->get_link($title, $url);
1160
1161
        $formatted_current_value = $current_value;
1162
        $formatted_expected_value = $expected_value;
1163
1164
        if ($formatter) {
1165
            if (method_exists($this, 'format_'.$formatter)) {
1166
                $formatted_current_value = call_user_func([$this, 'format_'.$formatter], $current_value);
1167
                $formatted_expected_value = call_user_func([$this, 'format_'.$formatter], $expected_value);
1168
            }
1169
        }
1170
1171
        return [$image, $section, $url, $formatted_current_value, $formatted_expected_value, $comment];
1172
    }
1173
1174
    /**
1175
     * Create an anchor HTML element.
1176
     *
1177
     * @param string $title
1178
     * @param string $url
1179
     *
1180
     * @return string
1181
     */
1182
    public function get_link($title, $url)
1183
    {
1184
        // Use about:blank (the legacy had a typo "about:bank")
1185
        return '<a href="'.$url.'" target="about:blank">'.$title.'</a>';
1186
    }
1187
1188
    /**
1189
     * @param int $value
1190
     *
1191
     * @return string
1192
     */
1193
    public function format_yes_no_optional($value)
1194
    {
1195
        $return = '';
1196
        switch ($value) {
1197
            case 0:
1198
                $return = get_lang('No');
1199
                break;
1200
            case 1:
1201
                $return = get_lang('Yes');
1202
                break;
1203
            case 2:
1204
                $return = get_lang('Optional');
1205
                break;
1206
        }
1207
1208
        return $return;
1209
    }
1210
1211
    /**
1212
     * @param mixed $value
1213
     *
1214
     * @return string
1215
     */
1216
    public function format_yes_no($value)
1217
    {
1218
        return $value ? get_lang('Yes') : get_lang('No');
1219
    }
1220
1221
    /**
1222
     * @param int $value
1223
     *
1224
     * @return string|int
1225
     */
1226
    public function format_on_off($value)
1227
    {
1228
        $value = intval($value);
1229
        if ($value > 1) {
1230
            // Greater than 1 values are shown "as-is", they may be interpreted as "On" later.
1231
            return $value;
1232
        }
1233
        // 'On'/'Off' as in php.ini; not translated.
1234
        return $value ? 'On' : 'Off';
1235
    }
1236
}
1237