Passed
Pull Request — master (#7060)
by
unknown
08:54
created

SettingsController::updateSetting()   F

Complexity

Conditions 19
Paths 505

Size

Total Lines 135
Code Lines 85

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 19
eloc 85
c 1
b 0
f 0
nc 505
nop 3
dl 0
loc 135
rs 1.0374

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
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.
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
74
        if (1 !== $currentUrlId) {
75
            return $this->json([
76
                'error' => 'Only the main URL (ID 1) can toggle this setting.',
77
            ], 403);
78
        }
79
80
        $payload = json_decode($request->getContent(), true);
81
82
        if (!\is_array($payload) || !isset($payload['variable'], $payload['status'])) {
83
            return $this->json([
84
                'error' => 'Invalid payload.',
85
            ], 400);
86
        }
87
88
        $variable = trim((string) $payload['variable']);
89
        $status = (int) $payload['status'];
90
        $status = $status === 1 ? 1 : 0;
91
92
        if ('' === $variable) {
93
            return $this->json([
94
                'error' => 'Invalid variable.',
95
            ], 400);
96
        }
97
98
        $repo = $this->entityManager->getRepository(SettingsCurrent::class);
99
100
        // We search by variable + current main AccessUrl entity.
101
        $setting = $repo->findOneBy([
102
            'variable' => $variable,
103
            'url' => $currentUrl,
104
        ]);
105
106
        if (!$setting) {
107
            return $this->json([
108
                'error' => 'Setting not found.',
109
            ], 404);
110
        }
111
112
        try {
113
            $setting->setAccessUrlChangeable($status);
114
            $this->entityManager->flush();
115
116
            return $this->json([
117
                'result' => 1,
118
                'status' => $status,
119
            ]);
120
        } catch (\Throwable $e) {
121
            return $this->json([
122
                'error' => 'Unable to update setting.',
123
                'details' => $e->getMessage(),
124
            ], 500);
125
        }
126
    }
127
128
    /**
129
     * Edit configuration with given namespace (search page).
130
     */
131
    #[IsGranted('ROLE_ADMIN')]
132
    #[Route('/settings/search_settings', name: 'chamilo_platform_settings_search')]
133
    public function searchSetting(Request $request, AccessUrlHelper $accessUrlHelper): Response
134
    {
135
        $manager = $this->getSettingsManager();
136
137
        $url = $accessUrlHelper->getCurrent();
138
        $manager->setUrl($url);
139
140
        $formList = [];
141
        $templateMap = [];
142
        $templateMapByCategory = [];
143
        $settings = [];
144
145
        $keyword = trim((string) $request->query->get('keyword', ''));
146
147
        $searchForm = $this->getSearchForm();
148
        $searchForm->handleRequest($request);
149
        if ($searchForm->isSubmitted() && $searchForm->isValid()) {
150
            $values = $searchForm->getData();
151
            $keyword = trim((string) ($values['keyword'] ?? ''));
152
        }
153
154
        $schemas = $manager->getSchemas();
155
        [$ordered, $labelMap] = $this->computeOrderedNamespacesByTranslatedLabel($schemas, $request);
156
157
        // Template map for current URL (existing behavior – JSON helper)
158
        $settingsRepo = $this->entityManager->getRepository(SettingsCurrent::class);
159
160
        // Build template map: current URL overrides main URL when missing.
161
        $currentUrlId = (int) $url->getId();
162
        $mainUrl = $this->entityManager->getRepository(AccessUrl::class)->find(1);
163
164
        if ($mainUrl instanceof AccessUrl && 1 !== $currentUrlId) {
165
            $mainRows = $settingsRepo->findBy(['url' => $mainUrl]);
166
            foreach ($mainRows as $s) {
167
                if ($s->getValueTemplate()) {
168
                    $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId();
169
                }
170
            }
171
        }
172
173
        $currentRows = $settingsRepo->findBy(['url' => $url]);
174
        foreach ($currentRows as $s) {
175
            if ($s->getValueTemplate()) {
176
                $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId();
177
            }
178
        }
179
180
        // MultiURL changeable flags: read from main URL (ID = 1) only
181
        $changeableMap = [];
182
        $mainUrlRows = $settingsRepo->createQueryBuilder('sc')
183
            ->join('sc.url', 'u')
184
            ->andWhere('u.id = :mainId')
185
            ->setParameter('mainId', 1)
186
            ->getQuery()
187
            ->getResult();
188
189
        foreach ($mainUrlRows as $row) {
190
            if ($row instanceof SettingsCurrent) {
191
                $changeableMap[$row->getVariable()] = $row->getAccessUrlChangeable();
192
            }
193
        }
194
195
        // Only platform admins on the main URL can toggle the MultiURL flag.
196
        $canToggleMultiUrlSetting = $this->isGranted('ROLE_ADMIN') && 1 === $currentUrlId;
197
198
        if ('' === $keyword) {
199
            return $this->render('@ChamiloCore/Admin/Settings/search.html.twig', [
200
                'keyword' => $keyword,
201
                'schemas' => $schemas,
202
                'settings' => $settings,
203
                'form_list' => $formList,
204
                'search_form' => $searchForm->createView(),
205
                'template_map' => $templateMap,
206
                'template_map_by_category' => $templateMapByCategory,
207
                'ordered_namespaces' => $ordered,
208
                'namespace_labels' => $labelMap,
209
                'changeable_map' => $changeableMap,
210
                'current_url_id' => $currentUrlId,
211
                'can_toggle_multiurl_setting' => $canToggleMultiUrlSetting,
212
            ]);
213
        }
214
215
        $settingsFromKeyword = $manager->getParametersFromKeywordOrderedByCategory($keyword);
216
        if (!empty($settingsFromKeyword)) {
217
            foreach ($settingsFromKeyword as $category => $parameterList) {
218
                if (empty($category)) {
219
                    continue;
220
                }
221
222
                $variablesInCategory = [];
223
                foreach ($parameterList as $parameter) {
224
                    $var = $parameter->getVariable();
225
                    $variablesInCategory[] = $var;
226
                    if (isset($templateMap[$var])) {
227
                        $templateMapByCategory[$category][$var] = $templateMap[$var];
228
                    }
229
                }
230
231
                // Convert category to schema alias and validate it BEFORE loading/creating the form
232
                $schemaAlias = $manager->convertNameSpaceToService($category);
233
234
                // skip unknown/legacy categories (e.g., "tools")
235
                if (!isset($schemas[$schemaAlias])) {
236
                    continue;
237
                }
238
239
                $settings = $manager->load($category);
240
                $form = $this->getSettingsFormFactory()->create($schemaAlias);
241
242
                foreach (array_keys($settings->getParameters()) as $name) {
243
                    if (!\in_array($name, $variablesInCategory, true)) {
244
                        $form->remove($name);
245
                        $settings->remove($name);
246
                    }
247
                }
248
                $form->setData($settings);
249
                $formList[$category] = $form->createView();
250
            }
251
        }
252
253
        return $this->render('@ChamiloCore/Admin/Settings/search.html.twig', [
254
            'keyword' => $keyword,
255
            'schemas' => $schemas,
256
            'settings' => $settings,
257
            'form_list' => $formList,
258
            'search_form' => $searchForm->createView(),
259
            'template_map' => $templateMap,
260
            'template_map_by_category' => $templateMapByCategory,
261
            'ordered_namespaces' => $ordered,
262
            'namespace_labels' => $labelMap,
263
            'changeable_map' => $changeableMap,
264
            'current_url_id' => $currentUrlId,
265
            'can_toggle_multiurl_setting' => $canToggleMultiUrlSetting,
266
        ]);
267
    }
268
269
    /**
270
     * Edit configuration with given namespace.
271
     */
272
    #[IsGranted('ROLE_ADMIN')]
273
    #[Route('/settings/{namespace}', name: 'chamilo_platform_settings')]
274
    public function updateSetting(Request $request, AccessUrlHelper $accessUrlHelper, string $namespace): Response
275
    {
276
        $manager = $this->getSettingsManager();
277
        $url = $accessUrlHelper->getCurrent();
278
        $manager->setUrl($url);
279
        $schemaAlias = $manager->convertNameSpaceToService($namespace);
280
281
        $keyword = (string) $request->query->get('keyword', '');
282
        // Extra diagnostics used only for the "search" namespace
283
        $searchDiagnostics = null;
284
285
        // Validate schema BEFORE load/create to avoid NonExistingServiceException
286
        $schemas = $manager->getSchemas();
287
        if (!isset($schemas[$schemaAlias])) {
288
            $this->addFlash('warning', \sprintf('Unknown settings category "%s". Showing Platform settings.', $namespace));
289
290
            return $this->redirectToRoute('chamilo_platform_settings', [
291
                'namespace' => 'platform',
292
            ]);
293
        }
294
295
        $settings = $manager->load($namespace);
296
297
        $form = $this->getSettingsFormFactory()->create(
298
            $schemaAlias,
299
            null,
300
            ['allow_extra_fields' => true]
301
        );
302
303
        $form->setData($settings);
304
305
        // Build extra diagnostics for Xapian and converters when editing "search" settings
306
        if ('search' === $namespace) {
307
            $searchDiagnostics = $this->buildSearchDiagnostics($manager);
308
        }
309
310
        $isPartial =
311
            $request->isMethod('PATCH')
312
            || 'PATCH' === strtoupper((string) $request->request->get('_method'))
313
            || $request->request->getBoolean('_partial', false);
314
315
        if ($isPartial) {
316
            $payload = $request->request->all($form->getName());
317
            $form->submit($payload, false);
318
        } else {
319
            $form->handleRequest($request);
320
        }
321
322
        if ($form->isSubmitted() && $form->isValid()) {
323
            $messageType = 'success';
324
325
            try {
326
                $manager->save($form->getData());
327
                $message = $this->trans('The settings have been stored');
328
            } catch (ValidatorException $validatorException) {
329
                $message = $this->trans($validatorException->getMessage());
330
                $messageType = 'error';
331
            }
332
333
            $this->addFlash($messageType, $message);
334
335
            if ('' !== $keyword) {
336
                return $this->redirectToRoute('chamilo_platform_settings_search', [
337
                    'keyword' => $keyword,
338
                ]);
339
            }
340
341
            return $this->redirectToRoute('chamilo_platform_settings', [
342
                'namespace' => $namespace,
343
            ]);
344
        }
345
346
        [$ordered, $labelMap] = $this->computeOrderedNamespacesByTranslatedLabel($schemas, $request);
347
348
        $templateMap = [];
349
        $settingsRepo = $this->entityManager->getRepository(SettingsCurrent::class);
350
351
        $currentUrlId = (int) $url->getId();
352
        $mainUrl = $this->entityManager->getRepository(AccessUrl::class)->find(1);
353
354
        // Build template map: fallback to main URL templates when sub-URL has no row for a locked setting.
355
        if ($mainUrl instanceof AccessUrl && 1 !== $currentUrlId) {
356
            $mainRows = $settingsRepo->findBy(['url' => $mainUrl]);
357
            foreach ($mainRows as $s) {
358
                if ($s->getValueTemplate()) {
359
                    $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId();
360
                }
361
            }
362
        }
363
364
        $settingsWithTemplate = $settingsRepo->findBy(['url' => $url]);
365
        foreach ($settingsWithTemplate as $s) {
366
            if ($s->getValueTemplate()) {
367
                $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId();
368
            }
369
        }
370
371
        // MultiURL changeable flags: read from main URL (ID = 1) only
372
        $changeableMap = [];
373
        $mainUrlRows = $settingsRepo->createQueryBuilder('sc')
374
            ->join('sc.url', 'u')
375
            ->andWhere('u.id = :mainId')
376
            ->setParameter('mainId', 1)
377
            ->getQuery()
378
            ->getResult();
379
380
        foreach ($mainUrlRows as $row) {
381
            if ($row instanceof SettingsCurrent) {
382
                $changeableMap[$row->getVariable()] = $row->getAccessUrlChangeable();
383
            }
384
        }
385
386
        $platform = [
387
            'server_type' => (string) $manager->getSetting('platform.server_type', true),
388
        ];
389
390
        // Only platform admins on the main URL can toggle the MultiURL flag.
391
        $canToggleMultiUrlSetting = $this->isGranted('ROLE_ADMIN') && 1 === $currentUrlId;
392
393
        return $this->render('@ChamiloCore/Admin/Settings/default.html.twig', [
394
            'schemas' => $schemas,
395
            'settings' => $settings,
396
            'form' => $form->createView(),
397
            'keyword' => $keyword,
398
            'search_form' => $this->getSearchForm()->createView(),
399
            'template_map' => $templateMap,
400
            'ordered_namespaces' => $ordered,
401
            'namespace_labels' => $labelMap,
402
            'platform' => $platform,
403
            'changeable_map' => $changeableMap,
404
            'current_url_id' => $currentUrlId,
405
            'can_toggle_multiurl_setting' => $canToggleMultiUrlSetting,
406
            'search_diagnostics' => $searchDiagnostics,
407
        ]);
408
    }
409
410
    /**
411
     * Sync settings from classes with the database.
412
     */
413
    #[IsGranted('ROLE_ADMIN')]
414
    #[Route('/settings_sync', name: 'sync_settings')]
415
    public function syncSettings(AccessUrlHelper $accessUrlHelper): Response
416
    {
417
        $manager = $this->getSettingsManager();
418
        $url = $accessUrlHelper->getCurrent();
419
        $manager->setUrl($url);
420
        $manager->installSchemas($url);
421
422
        return new Response('Updated');
423
    }
424
425
    #[IsGranted('ROLE_ADMIN')]
426
    #[Route('/settings/template/{id}', name: 'chamilo_platform_settings_template')]
427
    public function getTemplateExample(int $id): JsonResponse
428
    {
429
        $repo = $this->entityManager->getRepository(SettingsValueTemplate::class);
430
        $template = $repo->find($id);
431
432
        if (!$template) {
433
            return $this->json([
434
                'error' => $this->translator->trans('Template not found.'),
435
            ], Response::HTTP_NOT_FOUND);
436
        }
437
438
        return $this->json([
439
            'variable' => $template->getVariable(),
440
            'json_example' => $template->getJsonExample(),
441
            'description' => $template->getDescription(),
442
        ]);
443
    }
444
445
    /**
446
     * @return FormInterface
447
     */
448
    private function getSearchForm()
449
    {
450
        $builder = $this->container->get('form.factory')->createNamedBuilder('search');
451
        $builder->add('keyword', TextType::class);
452
        $builder->add('search', SubmitType::class, ['attr' => ['class' => 'btn btn--primary']]);
453
454
        return $builder->getForm();
455
    }
456
457
    private function computeOrderedNamespacesByTranslatedLabel(array $schemas, Request $request): array
458
    {
459
        // Extract raw namespaces from schema service ids
460
        $namespaces = array_map(
461
            static fn ($k) => str_replace('chamilo_core.settings.', '', $k),
462
            array_keys($schemas)
463
        );
464
465
        $transform = [
466
            'announcement' => 'Announcements',
467
            'attendance' => 'Attendances',
468
            'cas' => 'CAS',
469
            'certificate' => 'Certificates',
470
            'course' => 'Courses',
471
            'document' => 'Documents',
472
            'exercise' => 'Tests',
473
            'forum' => 'Forums',
474
            'group' => 'Groups',
475
            'language' => 'Internationalization',
476
            'lp' => 'Learning paths',
477
            'mail' => 'E-mail',
478
            'message' => 'Messages',
479
            'profile' => 'User profiles',
480
            'session' => 'Sessions',
481
            'skill' => 'Skills',
482
            'social' => 'Social network',
483
            'survey' => 'Surveys',
484
            'work' => 'Assignments',
485
            'ticket' => 'Support tickets',
486
            'tracking' => 'Reporting',
487
            'webservice' => 'Webservices',
488
            'catalog' => 'Catalogue',
489
            'catalogue' => 'Catalogue',
490
            'ai_helpers' => 'AI helpers',
491
        ];
492
493
        // Build label map (translated). For keys not in $transform, use Title Case of ns.
494
        $labelMap = [];
495
        foreach ($namespaces as $ns) {
496
            if (isset($transform[$ns])) {
497
                $labelMap[$ns] = $this->translator->trans($transform[$ns]);
498
            } else {
499
                $key = ucfirst(str_replace('_', ' ', $ns));
500
                $labelMap[$ns] = $this->translator->trans($key);
501
            }
502
        }
503
504
        // Sort by translated label (locale-aware)
505
        $collator = class_exists(Collator::class) ? new Collator($request->getLocale()) : null;
506
        usort($namespaces, function ($a, $b) use ($labelMap, $collator) {
507
            return $collator
508
                ? $collator->compare($labelMap[$a], $labelMap[$b])
509
                : strcasecmp($labelMap[$a], $labelMap[$b]);
510
        });
511
512
        // Optional: keep AI helpers near the top (second position)
513
        $idx = array_search('ai_helpers', $namespaces, true);
514
        if (false !== $idx) {
515
            array_splice($namespaces, $idx, 1);
516
            array_splice($namespaces, 1, 0, ['ai_helpers']);
517
        }
518
519
        return [$namespaces, $labelMap];
520
    }
521
522
    /**
523
     * Build environment diagnostics for the "search" settings page.
524
     */
525
    private function buildSearchDiagnostics(SettingsManager $manager): array
526
    {
527
        $searchEnabled = (string) $manager->getSetting('search.search_enabled');
528
529
        // Base status rows (Xapian extension + directory checks + custom fields)
530
        $indexDir = $this->searchIndexPathResolver->getIndexDir();
531
532
        $xapianLoaded = \extension_loaded('xapian');
533
        $dirExists = is_dir($indexDir);
534
        $dirWritable = is_writable($indexDir);
535
        $fieldsCount = $this->entityManager
536
            ->getRepository(SearchEngineField::class)
537
            ->count([])
538
        ;
539
540
        $statusRows = [
541
            [
542
                'label' => $this->translator->trans('Xapian module installed'),
543
                'ok' => $xapianLoaded,
544
            ],
545
            [
546
                'label' => $this->translator->trans('The directory exists').' - '.$indexDir,
547
                'ok' => $dirExists,
548
            ],
549
            [
550
                'label' => $this->translator->trans('Is writable').' - '.$indexDir,
551
                'ok' => $dirWritable,
552
            ],
553
            [
554
                'label' => $this->translator->trans('Available custom search fields'),
555
                'ok' => $fieldsCount > 0,
556
            ],
557
        ];
558
559
        // External converters (ps2pdf, pdftotext, ...)
560
        $tools = [];
561
        $toolsWarning = null;
562
563
        $isWindows = DIRECTORY_SEPARATOR === '\\';
564
565
        if ($isWindows) {
566
            $toolsWarning = $this->translator->trans(
567
                'You are using Chamilo on a Windows platform. Document conversion helpers are not available for full-text indexing.'
568
            );
569
        } else {
570
            $programs = ['ps2pdf', 'pdftotext', 'catdoc', 'html2text', 'unrtf', 'catppt', 'xls2csv'];
571
572
            foreach ($programs as $program) {
573
                $output = [];
574
                $returnVar = null;
575
576
                // Same behaviour as "which $program" in Chamilo 1
577
                @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

577
                /** @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...
578
                $path = $output[0] ?? '';
579
                $installed = '' !== $path;
580
581
                $tools[] = [
582
                    'name' => $program,
583
                    'path' => $path,
584
                    'ok' => $installed,
585
                ];
586
            }
587
        }
588
589
        return [
590
            // Whether full-text search is enabled at all
591
            'enabled' => ('true' === $searchEnabled),
592
            // Xapian + directory + custom fields
593
            'status_rows' => $statusRows,
594
            // External converters
595
            'tools' => $tools,
596
            'tools_warning' => $toolsWarning,
597
        ];
598
    }
599
}
600