Passed
Pull Request — master (#7204)
by
unknown
08:46
created

SettingsController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 5
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Controller\Admin;
8
9
use Chamilo\CoreBundle\Controller\BaseController;
10
use Chamilo\CoreBundle\Entity\AccessUrl;
11
use Chamilo\CoreBundle\Entity\SearchEngineField;
12
use Chamilo\CoreBundle\Entity\SettingsCurrent;
13
use Chamilo\CoreBundle\Entity\SettingsValueTemplate;
14
use Chamilo\CoreBundle\Helpers\AccessUrlHelper;
15
use Chamilo\CoreBundle\Search\Xapian\SearchIndexPathResolver;
16
use Chamilo\CoreBundle\Settings\SettingsManager;
17
use Chamilo\CoreBundle\Traits\ControllerTrait;
18
use Collator;
19
use Doctrine\ORM\EntityManagerInterface;
20
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
21
use Symfony\Component\Form\Extension\Core\Type\TextType;
22
use Symfony\Component\Form\FormInterface;
23
use Symfony\Component\HttpFoundation\JsonResponse;
24
use Symfony\Component\HttpFoundation\Request;
25
use Symfony\Component\HttpFoundation\Response;
26
use Symfony\Component\Routing\Attribute\Route;
27
use Symfony\Component\Security\Http\Attribute\IsGranted;
28
use Symfony\Component\Validator\Exception\ValidatorException;
29
use Symfony\Contracts\Translation\TranslatorInterface;
30
31
use const DIRECTORY_SEPARATOR;
32
33
#[Route('/admin')]
34
class SettingsController extends BaseController
35
{
36
    use ControllerTrait;
37
38
    public function __construct(
39
        private readonly EntityManagerInterface $entityManager,
40
        private readonly TranslatorInterface $translator,
41
        private readonly SearchIndexPathResolver $searchIndexPathResolver
42
    ) {}
43
44
    #[Route('/settings', name: 'admin_settings')]
45
    public function index(): Response
46
    {
47
        return $this->redirectToRoute('chamilo_platform_settings', ['namespace' => 'platform']);
48
    }
49
50
    /**
51
     * Toggle access_url_changeable for a given setting variable.
52
     * Only platform admins on the main URL (ID = 1) are allowed to change it.
53
     */
54
    #[IsGranted('ROLE_ADMIN')]
55
    #[Route('/settings/toggle_changeable', name: 'settings_toggle_changeable', methods: ['POST'])]
56
    public function toggleChangeable(Request $request, AccessUrlHelper $accessUrlHelper): JsonResponse
57
    {
58
        // Security: only admins (defense-in-depth; attribute already protects this route).
59
        if (!$this->isGranted('ROLE_ADMIN')) {
60
            return $this->json([
61
                'error' => 'Only platform admins can modify this flag.',
62
            ], 403);
63
        }
64
65
        $currentUrl = $accessUrlHelper->getCurrent();
66
        if (!$currentUrl) {
67
            return $this->json([
68
                'error' => 'Access URL not resolved.',
69
            ], 500);
70
        }
71
72
        $currentUrlId = (int) $currentUrl->getId();
73
        if (1 !== $currentUrlId) {
74
            return $this->json([
75
                'error' => 'Only the main URL (ID 1) can toggle this setting.',
76
            ], 403);
77
        }
78
79
        $payload = json_decode((string) $request->getContent(), true);
80
        if (!\is_array($payload)) {
81
            return $this->json([
82
                'error' => 'Invalid JSON payload.',
83
            ], 400);
84
        }
85
86
        $variable = isset($payload['variable']) ? trim((string) $payload['variable']) : '';
87
        $statusRaw = $payload['status'] ?? null;
88
89
        // Optional: category/namespace helps avoid collisions when the same variable exists in multiple schemas.
90
        $category = null;
91
        if (isset($payload['category'])) {
92
            $category = trim((string) $payload['category']);
93
        } elseif (isset($payload['namespace'])) {
94
            $category = trim((string) $payload['namespace']);
95
        }
96
97
        if ('' === $variable) {
98
            return $this->json([
99
                'error' => 'Missing "variable".',
100
            ], 400);
101
        }
102
103
        // Basic hardening: setting variable names are typically snake_case.
104
        if (!preg_match('/^[a-zA-Z0-9_]+$/', $variable)) {
105
            return $this->json([
106
                'error' => 'Invalid variable name.',
107
            ], 400);
108
        }
109
110
        $status = ((int) $statusRaw) === 1 ? 1 : 0;
111
112
        $repo = $this->entityManager->getRepository(SettingsCurrent::class);
113
114
        // Ensure we always update canonical rows on main URL (ID=1).
115
        $mainUrl = $this->entityManager->getRepository(AccessUrl::class)->find(1);
116
        if (!$mainUrl instanceof AccessUrl) {
117
            return $this->json([
118
                'error' => 'Main URL (ID 1) not found.',
119
            ], 500);
120
        }
121
122
        // Find rows: either a specific category, or all rows matching the variable on main URL.
123
        $rows = [];
124
        if (null !== $category && '' !== $category) {
125
            $rows = array_merge(
126
                $repo->findBy(['variable' => $variable, 'url' => $mainUrl, 'category' => $category]),
127
                $repo->findBy(['variable' => $variable, 'url' => $mainUrl, 'category' => ucfirst($category)])
128
            );
129
            // Remove duplicates
130
            $rows = array_values(array_unique($rows, SORT_REGULAR));
131
        } else {
132
            $rows = $repo->findBy(['variable' => $variable, 'url' => $mainUrl]);
133
        }
134
135
        if (empty($rows)) {
136
            return $this->json([
137
                'error' => 'Setting not found on main URL.',
138
            ], 404);
139
        }
140
141
        try {
142
            $updated = 0;
143
144
            foreach ($rows as $setting) {
145
                if (!$setting instanceof SettingsCurrent) {
146
                    continue;
147
                }
148
149
                // Locked settings must not be toggled (even on main URL).
150
                if (method_exists($setting, 'getAccessUrlLocked') && 1 === (int) $setting->getAccessUrlLocked()) {
151
                    return $this->json([
152
                        'error' => 'This setting is locked and cannot be toggled.',
153
                    ], 403);
154
                }
155
156
                $setting->setAccessUrlChangeable($status);
157
                $this->entityManager->persist($setting);
158
                $updated++;
159
            }
160
161
            $this->entityManager->flush();
162
163
            // Clear session schema caches so admin UI reflects the change immediately.
164
            if ($request->hasSession()) {
165
                $session = $request->getSession();
166
                foreach (array_keys((array) $session->all()) as $key) {
167
                    if ('schemas' === $key || str_starts_with((string) $key, 'schemas_url_')) {
168
                        $session->remove($key);
169
                    }
170
                }
171
            }
172
173
            return $this->json([
174
                'result' => 1,
175
                'variable' => $variable,
176
                'status' => $status,
177
                'updated_rows' => $updated,
178
            ]);
179
        } catch (\Throwable $e) {
180
            return $this->json([
181
                'error' => 'Unable to update setting.',
182
                'details' => $e->getMessage(),
183
            ], 500);
184
        }
185
    }
186
187
    /**
188
     * Edit configuration with given namespace (search page).
189
     */
190
    #[IsGranted('ROLE_ADMIN')]
191
    #[Route('/settings/search_settings', name: 'chamilo_platform_settings_search')]
192
    public function searchSetting(Request $request, AccessUrlHelper $accessUrlHelper): Response
193
    {
194
        $manager = $this->getSettingsManager();
195
196
        $url = $accessUrlHelper->getCurrent();
197
        $manager->setUrl($url);
198
199
        $formList = [];
200
        $templateMap = [];
201
        $templateMapByCategory = [];
202
        $settings = [];
203
204
        $keyword = trim((string) $request->query->get('keyword', ''));
205
206
        $searchForm = $this->getSearchForm();
207
        $searchForm->handleRequest($request);
208
        if ($searchForm->isSubmitted() && $searchForm->isValid()) {
209
            $values = $searchForm->getData();
210
            $keyword = trim((string) ($values['keyword'] ?? ''));
211
        }
212
213
        $schemas = $manager->getSchemas();
214
        [$ordered, $labelMap] = $this->computeOrderedNamespacesByTranslatedLabel($schemas, $request);
215
216
        $settingsRepo = $this->entityManager->getRepository(SettingsCurrent::class);
217
218
        $currentUrlId = (int) $url->getId();
219
        $mainUrl = $this->entityManager->getRepository(AccessUrl::class)->find(1);
220
221
        // Build template map: current URL overrides main URL when missing.
222
        if ($mainUrl instanceof AccessUrl && 1 !== $currentUrlId) {
223
            $mainRows = $settingsRepo->findBy(['url' => $mainUrl]);
224
            foreach ($mainRows as $s) {
225
                if ($s->getValueTemplate()) {
226
                    $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId();
227
                }
228
            }
229
        }
230
231
        $currentRows = $settingsRepo->findBy(['url' => $url]);
232
        foreach ($currentRows as $s) {
233
            if ($s->getValueTemplate()) {
234
                $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId();
235
            }
236
        }
237
238
        // MultiURL flags: read from main URL (ID = 1) only
239
        $changeableMap = [];
240
        $lockedMap = [];
241
242
        $mainUrlRows = $settingsRepo->createQueryBuilder('sc')
243
            ->join('sc.url', 'u')
244
            ->andWhere('u.id = :mainId')
245
            ->setParameter('mainId', 1)
246
            ->getQuery()
247
            ->getResult();
248
249
        foreach ($mainUrlRows as $row) {
250
            if ($row instanceof SettingsCurrent) {
251
                $changeableMap[$row->getVariable()] = (int) $row->getAccessUrlChangeable();
252
                $lockedMap[$row->getVariable()] = method_exists($row, 'getAccessUrlLocked')
253
                    ? (int) $row->getAccessUrlLocked()
254
                    : 0;
255
            }
256
        }
257
258
        // Only platform admins on the main URL can toggle the MultiURL flag.
259
        $canToggleMultiUrlSetting = $this->isGranted('ROLE_ADMIN') && 1 === $currentUrlId;
260
261
        if ('' === $keyword) {
262
            return $this->render('@ChamiloCore/Admin/Settings/search.html.twig', [
263
                'keyword' => $keyword,
264
                'schemas' => $schemas,
265
                'settings' => $settings,
266
                'form_list' => $formList,
267
                'search_form' => $searchForm->createView(),
268
                'template_map' => $templateMap,
269
                'template_map_by_category' => $templateMapByCategory,
270
                'ordered_namespaces' => $ordered,
271
                'namespace_labels' => $labelMap,
272
                'changeable_map' => $changeableMap,
273
                'locked_map' => $lockedMap,
274
                'current_url_id' => $currentUrlId,
275
                'can_toggle_multiurl_setting' => $canToggleMultiUrlSetting,
276
            ]);
277
        }
278
279
        $settingsFromKeyword = $manager->getParametersFromKeywordOrderedByCategory($keyword);
280
        if (!empty($settingsFromKeyword)) {
281
            foreach ($settingsFromKeyword as $category => $parameterList) {
282
                if (empty($category)) {
283
                    continue;
284
                }
285
286
                $variablesInCategory = [];
287
                foreach ($parameterList as $parameter) {
288
                    $var = $parameter->getVariable();
289
290
                    // Hide locked settings from child URLs (do not show them at all).
291
                    if (1 !== $currentUrlId && 1 === (int) ($lockedMap[$var] ?? 0)) {
292
                        continue;
293
                    }
294
295
                    $variablesInCategory[] = $var;
296
297
                    if (isset($templateMap[$var])) {
298
                        $templateMapByCategory[$category][$var] = $templateMap[$var];
299
                    }
300
                }
301
302
                $schemaAlias = $manager->convertNameSpaceToService($category);
303
304
                // Skip unknown/legacy categories (e.g., "tools")
305
                if (!isset($schemas[$schemaAlias])) {
306
                    continue;
307
                }
308
309
                $settings = $manager->load($category);
310
                $form = $this->getSettingsFormFactory()->create($schemaAlias);
311
312
                // Keep only keyword-matching variables, and also remove locked ones for child URLs.
313
                foreach (array_keys($settings->getParameters()) as $name) {
314
                    $isLockedForChild = (1 !== $currentUrlId) && (1 === (int) ($lockedMap[$name] ?? 0));
315
316
                    if ($isLockedForChild || !\in_array($name, $variablesInCategory, true)) {
317
                        if ($form->has($name)) {
318
                            $form->remove($name);
319
                        }
320
                        $settings->remove($name);
321
                    }
322
                }
323
324
                $form->setData($settings);
325
                $formList[$category] = $form->createView();
326
            }
327
        }
328
329
        return $this->render('@ChamiloCore/Admin/Settings/search.html.twig', [
330
            'keyword' => $keyword,
331
            'schemas' => $schemas,
332
            'settings' => $settings,
333
            'form_list' => $formList,
334
            'search_form' => $searchForm->createView(),
335
            'template_map' => $templateMap,
336
            'template_map_by_category' => $templateMapByCategory,
337
            'ordered_namespaces' => $ordered,
338
            'namespace_labels' => $labelMap,
339
            'changeable_map' => $changeableMap,
340
            'locked_map' => $lockedMap,
341
            'current_url_id' => $currentUrlId,
342
            'can_toggle_multiurl_setting' => $canToggleMultiUrlSetting,
343
        ]);
344
    }
345
346
    /**
347
     * Edit configuration with given namespace.
348
     */
349
    #[IsGranted('ROLE_ADMIN')]
350
    #[Route('/settings/{namespace}', name: 'chamilo_platform_settings')]
351
    public function updateSetting(Request $request, AccessUrlHelper $accessUrlHelper, string $namespace): Response
352
    {
353
        $manager = $this->getSettingsManager();
354
        $url = $accessUrlHelper->getCurrent();
355
        $manager->setUrl($url);
356
357
        $schemaAlias = $manager->convertNameSpaceToService($namespace);
358
359
        $keyword = (string) $request->query->get('keyword', '');
360
        $searchDiagnostics = null;
361
362
        // Validate schema BEFORE load/create to avoid NonExistingServiceException
363
        $schemas = $manager->getSchemas();
364
        if (!isset($schemas[$schemaAlias])) {
365
            $this->addFlash('warning', \sprintf('Unknown settings category "%s". Showing Platform settings.', $namespace));
366
367
            return $this->redirectToRoute('chamilo_platform_settings', [
368
                'namespace' => 'platform',
369
            ]);
370
        }
371
372
        $settingsRepo = $this->entityManager->getRepository(SettingsCurrent::class);
373
374
        $currentUrlId = (int) $url->getId();
375
        $mainUrl = $this->entityManager->getRepository(AccessUrl::class)->find(1);
376
377
        // MultiURL flags: read from main URL (ID = 1) only
378
        $changeableMap = [];
379
        $lockedMap = [];
380
381
        $mainUrlRows = $settingsRepo->createQueryBuilder('sc')
382
            ->join('sc.url', 'u')
383
            ->andWhere('u.id = :mainId')
384
            ->setParameter('mainId', 1)
385
            ->getQuery()
386
            ->getResult();
387
388
        foreach ($mainUrlRows as $row) {
389
            if ($row instanceof SettingsCurrent) {
390
                $changeableMap[$row->getVariable()] = (int) $row->getAccessUrlChangeable();
391
                $lockedMap[$row->getVariable()] = method_exists($row, 'getAccessUrlLocked')
392
                    ? (int) $row->getAccessUrlLocked()
393
                    : 0;
394
            }
395
        }
396
397
        $settings = $manager->load($namespace);
398
399
        $form = $this->getSettingsFormFactory()->create(
400
            $schemaAlias,
401
            null,
402
            ['allow_extra_fields' => true]
403
        );
404
405
        // Hide locked settings from child URLs (do not show them at all).
406
        if (1 !== $currentUrlId) {
407
            foreach (array_keys($settings->getParameters()) as $name) {
408
                if (1 === (int) ($lockedMap[$name] ?? 0)) {
409
                    if ($form->has($name)) {
410
                        $form->remove($name);
411
                    }
412
                    $settings->remove($name);
413
                }
414
            }
415
        }
416
417
        $form->setData($settings);
418
419
        // Build extra diagnostics for Xapian and converters when editing "search" settings
420
        if ('search' === $namespace) {
421
            $searchDiagnostics = $this->buildSearchDiagnostics($manager);
422
        }
423
424
        $isPartial =
425
            $request->isMethod('PATCH')
426
            || 'PATCH' === strtoupper((string) $request->request->get('_method'))
427
            || $request->request->getBoolean('_partial', false);
428
429
        if ($isPartial) {
430
            $payload = $request->request->all($form->getName());
431
            $form->submit($payload, false);
432
        } else {
433
            $form->handleRequest($request);
434
        }
435
436
        if ($form->isSubmitted() && $form->isValid()) {
437
            $messageType = 'success';
438
439
            try {
440
                $manager->save($form->getData());
441
                $message = $this->trans('The settings have been stored');
442
            } catch (ValidatorException $validatorException) {
443
                $message = $this->trans($validatorException->getMessage());
444
                $messageType = 'error';
445
            }
446
447
            $this->addFlash($messageType, $message);
448
449
            if ('' !== $keyword) {
450
                return $this->redirectToRoute('chamilo_platform_settings_search', [
451
                    'keyword' => $keyword,
452
                ]);
453
            }
454
455
            return $this->redirectToRoute('chamilo_platform_settings', [
456
                'namespace' => $namespace,
457
            ]);
458
        }
459
460
        [$ordered, $labelMap] = $this->computeOrderedNamespacesByTranslatedLabel($schemas, $request);
461
462
        $templateMap = [];
463
464
        // Build template map: fallback to main URL templates when sub-URL has no row for a locked setting.
465
        if ($mainUrl instanceof AccessUrl && 1 !== $currentUrlId) {
466
            $mainRows = $settingsRepo->findBy(['url' => $mainUrl]);
467
            foreach ($mainRows as $s) {
468
                if ($s->getValueTemplate()) {
469
                    $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId();
470
                }
471
            }
472
        }
473
474
        $settingsWithTemplate = $settingsRepo->findBy(['url' => $url]);
475
        foreach ($settingsWithTemplate as $s) {
476
            if ($s->getValueTemplate()) {
477
                $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId();
478
            }
479
        }
480
481
        $platform = [
482
            'server_type' => (string) $manager->getSetting('platform.server_type', true),
483
        ];
484
485
        // Only platform admins on the main URL can toggle the MultiURL flag.
486
        $canToggleMultiUrlSetting = $this->isGranted('ROLE_ADMIN') && 1 === $currentUrlId;
487
488
        return $this->render('@ChamiloCore/Admin/Settings/default.html.twig', [
489
            'schemas' => $schemas,
490
            'settings' => $settings,
491
            'form' => $form->createView(),
492
            'keyword' => $keyword,
493
            'search_form' => $this->getSearchForm()->createView(),
494
            'template_map' => $templateMap,
495
            'ordered_namespaces' => $ordered,
496
            'namespace_labels' => $labelMap,
497
            'platform' => $platform,
498
            'changeable_map' => $changeableMap,
499
            'locked_map' => $lockedMap,
500
            'current_url_id' => $currentUrlId,
501
            'can_toggle_multiurl_setting' => $canToggleMultiUrlSetting,
502
            'search_diagnostics' => $searchDiagnostics,
503
        ]);
504
    }
505
506
    /**
507
     * Sync settings from classes with the database.
508
     */
509
    #[IsGranted('ROLE_ADMIN')]
510
    #[Route('/settings_sync', name: 'sync_settings')]
511
    public function syncSettings(AccessUrlHelper $accessUrlHelper): Response
512
    {
513
        $manager = $this->getSettingsManager();
514
        $url = $accessUrlHelper->getCurrent();
515
        $manager->setUrl($url);
516
        $manager->installSchemas($url);
517
518
        return new Response('Updated');
519
    }
520
521
    #[IsGranted('ROLE_ADMIN')]
522
    #[Route('/settings/template/{id}', name: 'chamilo_platform_settings_template')]
523
    public function getTemplateExample(int $id): JsonResponse
524
    {
525
        $repo = $this->entityManager->getRepository(SettingsValueTemplate::class);
526
        $template = $repo->find($id);
527
528
        if (!$template) {
529
            return $this->json([
530
                'error' => $this->translator->trans('Template not found.'),
531
            ], Response::HTTP_NOT_FOUND);
532
        }
533
534
        return $this->json([
535
            'variable' => $template->getVariable(),
536
            'json_example' => $template->getJsonExample(),
537
            'description' => $template->getDescription(),
538
        ]);
539
    }
540
541
    /**
542
     * @return FormInterface
543
     */
544
    private function getSearchForm()
545
    {
546
        $builder = $this->container->get('form.factory')->createNamedBuilder('search');
547
        $builder->add('keyword', TextType::class);
548
        $builder->add('search', SubmitType::class, ['attr' => ['class' => 'btn btn--primary']]);
549
550
        return $builder->getForm();
551
    }
552
553
    private function computeOrderedNamespacesByTranslatedLabel(array $schemas, Request $request): array
554
    {
555
        // Extract raw namespaces from schema service ids
556
        $namespaces = array_map(
557
            static fn ($k) => str_replace('chamilo_core.settings.', '', $k),
558
            array_keys($schemas)
559
        );
560
561
        $transform = [
562
            'announcement' => 'Announcements',
563
            'attendance' => 'Attendances',
564
            'cas' => 'CAS',
565
            'certificate' => 'Certificates',
566
            'course' => 'Courses',
567
            'document' => 'Documents',
568
            'exercise' => 'Tests',
569
            'forum' => 'Forums',
570
            'group' => 'Groups',
571
            'language' => 'Internationalization',
572
            'lp' => 'Learning paths',
573
            'mail' => 'E-mail',
574
            'message' => 'Messages',
575
            'profile' => 'User profiles',
576
            'session' => 'Sessions',
577
            'skill' => 'Skills',
578
            'social' => 'Social network',
579
            'survey' => 'Surveys',
580
            'work' => 'Assignments',
581
            'ticket' => 'Support tickets',
582
            'tracking' => 'Reporting',
583
            'webservice' => 'Webservices',
584
            'catalog' => 'Catalogue',
585
            'catalogue' => 'Catalogue',
586
            'ai_helpers' => 'AI helpers',
587
        ];
588
589
        // Build label map (translated). For keys not in $transform, use Title Case of ns.
590
        $labelMap = [];
591
        foreach ($namespaces as $ns) {
592
            if (isset($transform[$ns])) {
593
                $labelMap[$ns] = $this->translator->trans($transform[$ns]);
594
            } else {
595
                $key = ucfirst(str_replace('_', ' ', $ns));
596
                $labelMap[$ns] = $this->translator->trans($key);
597
            }
598
        }
599
600
        // Sort by translated label (locale-aware)
601
        $collator = class_exists(Collator::class) ? new Collator($request->getLocale()) : null;
602
        usort($namespaces, function ($a, $b) use ($labelMap, $collator) {
603
            return $collator
604
                ? $collator->compare($labelMap[$a], $labelMap[$b])
605
                : strcasecmp($labelMap[$a], $labelMap[$b]);
606
        });
607
608
        // Optional: keep AI helpers near the top (second position)
609
        $idx = array_search('ai_helpers', $namespaces, true);
610
        if (false !== $idx) {
611
            array_splice($namespaces, $idx, 1);
612
            array_splice($namespaces, 1, 0, ['ai_helpers']);
613
        }
614
615
        return [$namespaces, $labelMap];
616
    }
617
618
    /**
619
     * Build environment diagnostics for the "search" settings page.
620
     */
621
    private function buildSearchDiagnostics(SettingsManager $manager): array
622
    {
623
        $searchEnabled = (string) $manager->getSetting('search.search_enabled');
624
625
        // Base status rows (Xapian extension + directory checks + custom fields)
626
        $indexDir = $this->searchIndexPathResolver->getIndexDir();
627
628
        $xapianLoaded = \extension_loaded('xapian');
629
        $dirExists = is_dir($indexDir);
630
        $dirWritable = is_writable($indexDir);
631
        $fieldsCount = $this->entityManager
632
            ->getRepository(SearchEngineField::class)
633
            ->count([])
634
        ;
635
636
        $statusRows = [
637
            [
638
                'label' => $this->translator->trans('Xapian module installed'),
639
                'ok' => $xapianLoaded,
640
            ],
641
            [
642
                'label' => $this->translator->trans('The directory exists').' - '.$indexDir,
643
                'ok' => $dirExists,
644
            ],
645
            [
646
                'label' => $this->translator->trans('Is writable').' - '.$indexDir,
647
                'ok' => $dirWritable,
648
            ],
649
            [
650
                'label' => $this->translator->trans('Available custom search fields'),
651
                'ok' => $fieldsCount > 0,
652
            ],
653
        ];
654
655
        // External converters (ps2pdf, pdftotext, ...)
656
        $tools = [];
657
        $toolsWarning = null;
658
659
        $isWindows = DIRECTORY_SEPARATOR === '\\';
660
661
        if ($isWindows) {
662
            $toolsWarning = $this->translator->trans(
663
                'You are using Chamilo on a Windows platform. Document conversion helpers are not available for full-text indexing.'
664
            );
665
        } else {
666
            $programs = ['ps2pdf', 'pdftotext', 'catdoc', 'html2text', 'unrtf', 'catppt', 'xls2csv'];
667
668
            foreach ($programs as $program) {
669
                $output = [];
670
                $returnVar = null;
671
672
                // Same behaviour as "which $program" in Chamilo 1
673
                @exec('which '.escapeshellarg($program), $output, $returnVar);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for exec(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

673
                /** @scrutinizer ignore-unhandled */ @exec('which '.escapeshellarg($program), $output, $returnVar);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
674
                $path = $output[0] ?? '';
675
                $installed = '' !== $path;
676
677
                $tools[] = [
678
                    'name' => $program,
679
                    'path' => $path,
680
                    'ok' => $installed,
681
                ];
682
            }
683
        }
684
685
        return [
686
            // Whether full-text search is enabled at all
687
            'enabled' => ('true' === $searchEnabled),
688
            // Xapian + directory + custom fields
689
            'status_rows' => $statusRows,
690
            // External converters
691
            'tools' => $tools,
692
            'tools_warning' => $toolsWarning,
693
        ];
694
    }
695
}
696