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