Passed
Pull Request — master (#7060)
by
unknown
09:22
created

SettingsController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

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