Passed
Pull Request — master (#7060)
by Yannick
09:27
created

SettingsController::toggleChangeable()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 59
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 38
c 0
b 0
f 0
nc 8
nop 2
dl 0
loc 59
rs 8.3786

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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

548
                /** @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...
549
                $path = $output[0] ?? '';
550
                $installed = '' !== $path;
551
552
                $tools[] = [
553
                    'name' => $program,
554
                    'path' => $path,
555
                    'ok' => $installed,
556
                ];
557
            }
558
        }
559
560
        return [
561
            // Whether full-text search is enabled at all
562
            'enabled' => ('true' === $searchEnabled),
563
            // Xapian + directory + custom fields
564
            'status_rows' => $statusRows,
565
            // External converters
566
            'tools' => $tools,
567
            'tools_warning' => $toolsWarning,
568
        ];
569
    }
570
}
571