SettingsController   F
last analyzed

Complexity

Total Complexity 90

Size/Duplication

Total Lines 662
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 390
dl 0
loc 662
rs 2
c 0
b 0
f 0
wmc 90

10 Methods

Rating   Name   Duplication   Size   Complexity  
A index() 0 4 1
F updateSetting() 0 155 24
A getSearchForm() 0 7 1
F toggleChangeable() 0 130 24
A syncSettings() 0 10 1
A buildSearchDiagnostics() 0 72 3
B computeOrderedNamespacesByTranslatedLabel() 0 63 6
A getTemplateExample() 0 17 2
A __construct() 0 5 1
F searchSetting() 0 154 27

How to fix   Complexity   

Complex Class

Complex classes like SettingsController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SettingsController, and based on these observations, apply Extract Interface, too.

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

677
                /** @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...
678
                $path = $output[0] ?? '';
679
                $installed = '' !== $path;
680
681
                $tools[] = [
682
                    'name' => $program,
683
                    'path' => $path,
684
                    'ok' => $installed,
685
                ];
686
            }
687
        }
688
689
        return [
690
            // Whether full-text search is enabled at all
691
            'enabled' => ('true' === $searchEnabled),
692
            // Xapian + directory + custom fields
693
            'status_rows' => $statusRows,
694
            // External converters
695
            'tools' => $tools,
696
            'tools_warning' => $toolsWarning,
697
        ];
698
    }
699
}
700