Passed
Pull Request — master (#7129)
by
unknown
08:39
created

CourseController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 3
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Controller;
8
9
use Chamilo\CoreBundle\Entity\Course;
10
use Chamilo\CoreBundle\Entity\CourseRelUser;
11
use Chamilo\CoreBundle\Entity\ExtraField;
12
use Chamilo\CoreBundle\Entity\SequenceResource;
13
use Chamilo\CoreBundle\Entity\Session;
14
use Chamilo\CoreBundle\Entity\SessionRelUser;
15
use Chamilo\CoreBundle\Entity\Tag;
16
use Chamilo\CoreBundle\Entity\Tool;
17
use Chamilo\CoreBundle\Entity\User;
18
use Chamilo\CoreBundle\Framework\Container;
19
use Chamilo\CoreBundle\Helpers\AccessUrlHelper;
20
use Chamilo\CoreBundle\Helpers\CidReqHelper;
21
use Chamilo\CoreBundle\Helpers\CourseHelper;
22
use Chamilo\CoreBundle\Helpers\UserHelper;
23
use Chamilo\CoreBundle\Repository\AssetRepository;
24
use Chamilo\CoreBundle\Repository\CourseCategoryRepository;
25
use Chamilo\CoreBundle\Repository\ExtraFieldValuesRepository;
26
use Chamilo\CoreBundle\Repository\LanguageRepository;
27
use Chamilo\CoreBundle\Repository\LegalRepository;
28
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
29
use Chamilo\CoreBundle\Repository\Node\IllustrationRepository;
30
use Chamilo\CoreBundle\Repository\SequenceResourceRepository;
31
use Chamilo\CoreBundle\Repository\TagRepository;
32
use Chamilo\CoreBundle\Security\Authorization\Voter\CourseVoter;
33
use Chamilo\CoreBundle\Settings\SettingsManager;
34
use Chamilo\CoreBundle\Tool\ToolChain;
35
use Chamilo\CourseBundle\Controller\ToolBaseController;
36
use Chamilo\CourseBundle\Entity\CBlog;
37
use Chamilo\CourseBundle\Entity\CCourseDescription;
38
use Chamilo\CourseBundle\Entity\CLink;
39
use Chamilo\CourseBundle\Entity\CShortcut;
40
use Chamilo\CourseBundle\Entity\CThematicAdvance;
41
use Chamilo\CourseBundle\Entity\CTool;
42
use Chamilo\CourseBundle\Entity\CToolIntro;
43
use Chamilo\CourseBundle\Repository\CCourseDescriptionRepository;
44
use Chamilo\CourseBundle\Repository\CLpRepository;
45
use Chamilo\CourseBundle\Repository\CQuizRepository;
46
use Chamilo\CourseBundle\Repository\CShortcutRepository;
47
use Chamilo\CourseBundle\Repository\CThematicRepository;
48
use Chamilo\CourseBundle\Repository\CToolRepository;
49
use Chamilo\CourseBundle\Settings\SettingsCourseManager;
50
use Chamilo\CourseBundle\Settings\SettingsFormFactory;
51
use CourseManager;
52
use Database;
53
use DateTimeInterface;
54
use Display;
55
use Doctrine\ORM\EntityManagerInterface;
56
use Event;
57
use Exception;
58
use Exercise;
59
use ExtraFieldValue;
60
use Graphp\GraphViz\GraphViz;
61
use IntlDateFormatter;
62
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
63
use Symfony\Bundle\SecurityBundle\Security;
64
use Symfony\Component\HttpFoundation\JsonResponse;
65
use Symfony\Component\HttpFoundation\RedirectResponse;
66
use Symfony\Component\HttpFoundation\Request;
67
use Symfony\Component\HttpFoundation\Response;
68
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
69
use Symfony\Component\Routing\Attribute\Route;
70
use Symfony\Component\Security\Http\Attribute\IsGranted;
71
use Symfony\Component\Serializer\SerializerInterface;
72
use Symfony\Component\Validator\Exception\ValidatorException;
73
use Symfony\Contracts\Translation\TranslatorInterface;
74
use UserManager;
75
76
/**
77
 * @author Julio Montoya <[email protected]>
78
 */
79
#[Route('/course')]
80
class CourseController extends ToolBaseController
81
{
82
    public function __construct(
83
        private readonly EntityManagerInterface $em,
84
        private readonly SerializerInterface $serializer,
85
        private readonly UserHelper $userHelper,
86
    ) {}
87
88
    #[IsGranted('ROLE_USER')]
89
    #[Route('/{cid}/checkLegal.json', name: 'chamilo_core_course_check_legal_json')]
90
    public function checkTermsAndConditionJson(
91
        Request $request,
92
        LegalRepository $legalTermsRepo,
93
        LanguageRepository $languageRepository,
94
        ExtraFieldValuesRepository $extraFieldValuesRepository,
95
        SettingsManager $settingsManager
96
    ): Response {
97
        $user = $this->userHelper->getCurrent();
98
        $course = $this->getCourse();
99
        $sid = $this->getSessionId();
100
101
        $responseData = [
102
            'redirect' => false,
103
            'url' => '#',
104
        ];
105
106
        if ($user->isStudent()
107
            && 'true' === $settingsManager->getSetting('registration.allow_terms_conditions', true)
108
            && 'course' === $settingsManager->getSetting('workflows.load_term_conditions_section', true)
109
        ) {
110
            $termAndConditionStatus = false;
111
            $extraValue = $extraFieldValuesRepository->findLegalAcceptByItemId($user->getId());
112
            if (!empty($extraValue['value'])) {
113
                $result = $extraValue['value'];
114
                $userConditions = explode(':', $result);
115
                $version = $userConditions[0];
116
                $langId = (int) $userConditions[1];
117
                $realVersion = $legalTermsRepo->getLastVersion($langId);
118
                $termAndConditionStatus = ($version >= $realVersion);
119
            }
120
121
            if (false === $termAndConditionStatus) {
122
                $request->getSession()->set('term_and_condition', ['user_id' => $user->getId()]);
123
124
                $redirect = true;
125
126
                if ('true' === $settingsManager->getSetting('course.allow_public_course_with_no_terms_conditions', true)
127
                    && Course::OPEN_WORLD === $course->getVisibility()
128
                ) {
129
                    $redirect = false;
130
                }
131
132
                if ($redirect && !$this->isGranted('ROLE_ADMIN')) {
133
                    $request->getSession()->remove('cid');
134
                    $request->getSession()->remove('course');
135
136
                    // Build return URL
137
                    $returnUrl = '/course/'.$course->getId().'/home?sid='.$sid;
138
139
                    $responseData = [
140
                        'redirect' => true,
141
                        'url' => '/main/auth/tc.php?return='.urlencode($returnUrl),
142
                    ];
143
                }
144
            } else {
145
                $request->getSession()->remove('term_and_condition');
146
            }
147
        }
148
149
        return new JsonResponse($responseData);
150
    }
151
152
    #[Route('/{cid}/home.json', name: 'chamilo_core_course_home_json')]
153
    public function indexJson(
154
        Request $request,
155
        CShortcutRepository $shortcutRepository,
156
        EntityManagerInterface $em,
157
        AssetRepository $assetRepository
158
    ): Response {
159
        // Handle drag & drop sort for course tools
160
        if ($request->isMethod('POST')) {
161
            $requestData = json_decode($request->getContent() ?: '', true) ?? [];
162
163
            if (isset($requestData['toolId'], $requestData['index'])) {
164
                $course = $this->getCourse();
165
                if (null === $course) {
166
                    return $this->json(
167
                        ['success' => false, 'message' => 'Course not found.'],
168
                        Response::HTTP_BAD_REQUEST
169
                    );
170
                }
171
172
                $sessionId = $this->getSessionId();
173
                $toolId = (int) $requestData['toolId'];
174
                $newIndex = (int) $requestData['index'];
175
176
                $result = $this->reorderCourseTools($em, $course, $sessionId, $toolId, $newIndex);
177
                $statusCode = $result['success'] ? Response::HTTP_OK : Response::HTTP_BAD_REQUEST;
178
179
                return $this->json($result, $statusCode);
180
            }
181
        }
182
183
        $course = $this->getCourse();
184
        $sessionId = $this->getSessionId();
185
        $isInASession = $sessionId > 0;
186
187
        if (null === $course) {
188
            throw $this->createAccessDeniedException();
189
        }
190
191
        if (empty($sessionId)) {
192
            $this->denyAccessUnlessGranted(CourseVoter::VIEW, $course);
193
        }
194
195
        $sessionHandler = $request->getSession();
196
197
        $userId = 0;
198
199
        $user = $this->userHelper->getCurrent();
200
        if (null !== $user) {
201
            $userId = $user->getId();
202
        }
203
204
        $courseCode = $course->getCode();
205
        $courseId = $course->getId();
206
207
        if ($user && $user->isInvitee()) {
208
            $isSubscribed = CourseManager::is_user_subscribed_in_course(
209
                $userId,
210
                $courseCode,
211
                $isInASession,
212
                $sessionId
213
            );
214
215
            if (!$isSubscribed) {
216
                throw $this->createAccessDeniedException();
217
            }
218
        }
219
220
        $isSpecialCourse = CourseManager::isSpecialCourse($courseId);
221
222
        if ($user && $isSpecialCourse && (isset($_GET['autoreg']) && 1 === (int) $_GET['autoreg'])
223
            && CourseManager::subscribeUser($userId, $courseId, STUDENT)
224
        ) {
225
            $sessionHandler->set('is_allowed_in_course', true);
226
        }
227
228
        $logInfo = [
229
            'tool' => 'course-main',
230
        ];
231
        Event::registerLog($logInfo);
232
233
        // Deleting the objects
234
        $sessionHandler->remove('toolgroup');
235
        $sessionHandler->remove('_gid');
236
        $sessionHandler->remove('oLP');
237
        $sessionHandler->remove('lpobject');
238
239
        api_remove_in_gradebook();
240
        Exercise::cleanSessionVariables();
241
242
        $shortcuts = [];
243
        if (null !== $user) {
244
            $shortcutQuery = $shortcutRepository->getResources($course->getResourceNode());
245
            $shortcuts = $shortcutQuery->getQuery()->getResult();
246
247
            $courseNodeId = $course->getResourceNode()->getId();
248
            $cid = $course->getId();
249
            $sid = $this->getSessionId() ?: null;
250
251
            /** @var CShortcut $shortcut */
252
            /** @var CShortcut $shortcut */
253
            foreach ($shortcuts as $shortcut) {
254
                $resourceNode = $shortcut->getShortCutNode();
255
256
                // Try as CLink
257
                $cLink = $em->getRepository(CLink::class)->findOneBy(['resourceNode' => $resourceNode]);
258
                if ($cLink) {
259
                    // Image (if any)
260
                    $shortcut->setCustomImageUrl(
261
                        $cLink->getCustomImage()
262
                            ? $assetRepository->getAssetUrl($cLink->getCustomImage())
263
                            : null
264
                    );
265
266
                    // External link behavior
267
                    $shortcut->setUrlOverride($cLink->getUrl()); // open external URL
268
                    $shortcut->setIcon(null);                    // keep default icon for links
269
                    $shortcut->target = $cLink->getTarget();     // e.g. "_blank"
270
271
                    continue;
272
                }
273
274
                // Try as CBlog
275
                $cBlog = $em->getRepository(CBlog::class)
276
                    ->findOneBy(['resourceNode' => $resourceNode])
277
                ;
278
279
                if ($cBlog) {
280
                    $courseNodeId = $course->getResourceNode()->getId();
281
                    $cid = $course->getId();
282
                    $sid = $this->getSessionId() ?: null;
283
284
                    $qs = http_build_query(array_filter([
285
                        'cid' => $cid,
286
                        'sid' => $sid ?: null,
287
                        'gid' => 0,
288
                    ], static fn ($v) => null !== $v));
289
290
                    $shortcut->setUrlOverride(\sprintf(
291
                        '/resources/blog/%d/%d/posts?%s',
292
                        $courseNodeId,
293
                        $cBlog->getIid(),
294
                        $qs
295
                    ));
296
                    $shortcut->setIcon('mdi-notebook-outline');  // blog icon
297
                    $shortcut->setCustomImageUrl(null);          // blogs use icon by default
298
                    $shortcut->target = '_self';
299
300
                    continue;
301
                }
302
303
                // Fallback
304
                $shortcut->setCustomImageUrl(null);
305
                $shortcut->setUrlOverride(null);
306
                $shortcut->setIcon(null);
307
                $shortcut->target = '_self';
308
            }
309
        }
310
        $responseData = [
311
            'shortcuts' => $shortcuts,
312
            'diagram' => '',
313
        ];
314
315
        $json = $this->serializer->serialize(
316
            $responseData,
317
            'json',
318
            [
319
                'groups' => ['course:read', 'ctool:read', 'tool:read', 'cshortcut:read'],
320
            ]
321
        );
322
323
        return new Response(
324
            $json,
325
            Response::HTTP_OK,
326
            [
327
                'Content-type' => 'application/json',
328
            ]
329
        );
330
    }
331
332
    #[Route('/{cid}/thematic_progress.json', name: 'chamilo_core_course_thematic_progress_json', methods: ['GET'])]
333
    public function thematicProgressJson(
334
        Request $request,
335
        CThematicRepository $thematicRepository,
336
        UserHelper $userHelper,
337
        CidReqHelper $cidReqHelper,
338
        TranslatorInterface $translator,
339
        SettingsCourseManager $courseSettingsManager
340
    ): JsonResponse {
341
        $course = $this->getCourse();
342
        if (null === $course) {
343
            throw $this->createAccessDeniedException();
344
        }
345
346
        if (0 === $this->getSessionId()) {
347
            $this->denyAccessUnlessGranted(CourseVoter::VIEW, $course);
348
        }
349
350
        $courseSettingsManager->setCourse($course);
351
        $displayMode = (string) $courseSettingsManager->getCourseSettingValue('display_info_advance_inside_homecourse');
352
353
        if ('' === $displayMode || '0' === $displayMode || '4' === $displayMode) {
354
            return new JsonResponse(['enabled' => false]);
355
        }
356
357
        $sessionEntity = $cidReqHelper->getSessionEntity();
358
        $currentUser = $userHelper->getCurrent();
359
360
        $advance1 = null;
361
        $advance2 = null;
362
        $subtitle1 = '';
363
        $subtitle2 = '';
364
365
        if ('1' === $displayMode) {
366
            // Last completed topic only
367
            $advance1 = $thematicRepository->findLastDoneAdvanceForCourse($course, $sessionEntity);
368
            if (null !== $advance1) {
369
                $subtitle1 = $translator->trans('Current topic');
370
            }
371
        } elseif ('2' === $displayMode) {
372
            // Two next not done topics
373
            $nextList = $thematicRepository->findNextNotDoneAdvancesForCourse($course, $sessionEntity, 2);
374
375
            if (isset($nextList[0]) && $nextList[0] instanceof CThematicAdvance) {
376
                $advance1 = $nextList[0];
377
                $subtitle1 = $translator->trans('Next topic');
378
            }
379
380
            if (isset($nextList[1]) && $nextList[1] instanceof CThematicAdvance) {
381
                $advance2 = $nextList[1];
382
                $subtitle2 = $translator->trans('Next topic');
383
            }
384
        } elseif ('3' === $displayMode) {
385
            // Current (last done) + next not done
386
            $advance1 = $thematicRepository->findLastDoneAdvanceForCourse($course, $sessionEntity);
387
            $nextList = $thematicRepository->findNextNotDoneAdvancesForCourse($course, $sessionEntity, 1);
388
389
            if (null !== $advance1) {
390
                $subtitle1 = $translator->trans('Current topic');
391
            }
392
393
            if (isset($nextList[0]) && $nextList[0] instanceof CThematicAdvance) {
394
                $advance2 = $nextList[0];
395
                $subtitle2 = $translator->trans('Next topic');
396
            }
397
        } else {
398
            return new JsonResponse(['enabled' => false]);
399
        }
400
401
        if (null === $advance1 && null === $advance2) {
402
            return new JsonResponse(['enabled' => false]);
403
        }
404
405
        $locale = $request->getLocale();
406
        $timezoneId = null;
407
408
        if ($currentUser && method_exists($currentUser, 'getTimezone') && $currentUser->getTimezone()) {
409
            $timezoneId = $currentUser->getTimezone();
410
        }
411
412
        if (empty($timezoneId)) {
413
            $timezoneId = date_default_timezone_get();
414
        }
415
416
        $dateFormatter = new IntlDateFormatter(
417
            $locale,
418
            IntlDateFormatter::MEDIUM,
419
            IntlDateFormatter::SHORT,
420
            $timezoneId
421
        );
422
423
        $buildItem = function (CThematicAdvance $advance, string $type, string $label) use ($dateFormatter): array {
424
            $thematic = $advance->getThematic();
425
426
            $startDate = $advance->getStartDate();
427
            $formattedDate = $startDate instanceof DateTimeInterface
428
                ? (string) $dateFormatter->format($startDate)
429
                : '';
430
431
            return [
432
                'type' => $type,
433
                'label' => $label,
434
                'title' => strip_tags($thematic->getTitle() ?? ''),
435
                'startDate' => $formattedDate,
436
                'content' => strip_tags($advance->getContent() ?? ''),
437
                'duration' => (float) $advance->getDuration(),
438
            ];
439
        };
440
441
        $items = [];
442
443
        if (null !== $advance1) {
444
            $firstType = ('1' === $displayMode || '3' === $displayMode) ? 'current' : 'next';
445
            $items[] = $buildItem($advance1, $firstType, $subtitle1);
446
        }
447
448
        if (null !== $advance2) {
449
            $items[] = $buildItem($advance2, 'next', $subtitle2);
450
        }
451
452
        $userPayload = null;
453
        if ($currentUser) {
454
            $name = method_exists($currentUser, 'getCompleteName')
455
                ? $currentUser->getCompleteName()
456
                : trim(\sprintf('%s %s', $currentUser->getFirstname(), $currentUser->getLastname()));
457
458
            $userPayload = [
459
                'name' => $name,
460
                'avatar' => null,
461
            ];
462
        }
463
464
        $thematicUrl = '/main/course_progress/index.php?cid='.$course->getId().'&sid='.$this->getSessionId().'&action=thematic_details';
465
        $thematicScoreRaw = $thematicRepository->calculateTotalAverageForCourse($course, $sessionEntity);
466
        $thematicScore = $thematicScoreRaw.'%';
467
468
        $payload = [
469
            'enabled' => true,
470
            'displayMode' => (int) $displayMode,
471
            'title' => $translator->trans('Thematic advance'),
472
            'score' => $thematicScore,
473
            'scoreRaw' => $thematicScoreRaw,
474
            'user' => $userPayload,
475
            'items' => $items,
476
            'detailUrl' => $thematicUrl,
477
            'labels' => [
478
                'duration' => $translator->trans('Duration in hours'),
479
                'seeDetail' => $translator->trans('See detail'),
480
            ],
481
        ];
482
483
        return new JsonResponse($payload);
484
    }
485
486
    #[Route('/{courseId}/next-course', name: 'chamilo_course_next_course')]
487
    public function getNextCourse(
488
        int $courseId,
489
        Request $request,
490
        SequenceResourceRepository $repo,
491
        Security $security,
492
        SettingsManager $settingsManager,
493
        EntityManagerInterface $em
494
    ): JsonResponse {
495
        $sessionId = $request->query->getInt('sid');
496
        $useDependents = $request->query->getBoolean('dependents', false);
497
        $user = $security->getUser();
498
        $userId = $user->getId();
499
500
        if ($useDependents) {
501
            $sequences = $repo->getDependents($courseId, SequenceResource::COURSE_TYPE);
502
            $checked = $repo->checkDependentsForUser($sequences, SequenceResource::COURSE_TYPE, $userId, $sessionId, $courseId);
503
            $isUnlocked = $repo->checkSequenceAreCompleted($checked);
504
            $sequenceResource = $repo->findRequirementForResource($courseId, SequenceResource::COURSE_TYPE);
505
        } else {
506
            $sequences = $repo->getRequirements($courseId, SequenceResource::COURSE_TYPE);
507
508
            $hasValidRequirement = false;
509
            foreach ($sequences as $sequence) {
510
                foreach ($sequence['requirements'] ?? [] as $resource) {
511
                    if ($resource instanceof Course) {
512
                        $hasValidRequirement = true;
513
514
                        break 2;
515
                    }
516
                }
517
            }
518
519
            if (!$hasValidRequirement) {
520
                return new JsonResponse([]);
521
            }
522
523
            $checked = $repo->checkRequirementsForUser($sequences, SequenceResource::COURSE_TYPE, $userId, $sessionId);
524
            $isUnlocked = $repo->checkSequenceAreCompleted($checked);
525
            $sequenceResource = $repo->findRequirementForResource($courseId, SequenceResource::COURSE_TYPE);
526
        }
527
528
        $graphImage = null;
529
530
        if ($sequenceResource && $sequenceResource->hasGraph()) {
531
            $graph = $sequenceResource->getSequence()->getUnSerializeGraph();
532
            if (null !== $graph) {
533
                $graph->setAttribute('graphviz.node.fontname', 'arial');
534
                $graphviz = new GraphViz();
535
                $graphImage = $graphviz->createImageSrc($graph);
536
            }
537
        }
538
539
        return new JsonResponse([
540
            'sequenceList' => array_values($checked),
541
            'allowSubscription' => $isUnlocked,
542
            'graph' => $graphImage,
543
        ]);
544
    }
545
546
    /**
547
     * Redirects the page to a tool, following the tools settings.
548
     */
549
    #[Route('/{cid}/tool/{toolName}', name: 'chamilo_core_course_redirect_tool')]
550
    public function redirectTool(
551
        Request $request,
552
        string $toolName,
553
        CToolRepository $repo,
554
        ToolChain $toolChain
555
    ): RedirectResponse {
556
        /** @var CTool|null $tool */
557
        $tool = $repo->findOneBy([
558
            'title' => $toolName,
559
        ]);
560
561
        if (null === $tool) {
562
            throw new NotFoundHttpException($this->trans('Tool not found'));
563
        }
564
565
        $tool = $toolChain->getToolFromName($tool->getTool()->getTitle());
566
        $link = $tool->getLink();
567
568
        if (null === $this->getCourse()) {
569
            throw new NotFoundHttpException($this->trans('Course not found'));
570
        }
571
        $optionalParams = '';
572
573
        $optionalParams = $request->query->get('cert') ? '&cert='.$request->query->get('cert') : '';
574
575
        if (strpos($link, 'nodeId')) {
576
            $nodeId = (string) $this->getCourse()->getResourceNode()->getId();
577
            $link = str_replace(':nodeId', $nodeId, $link);
578
        }
579
580
        $url = $link.'?'.$this->getCourseUrlQuery().$optionalParams;
581
582
        return $this->redirect($url);
583
    }
584
585
    /*public function redirectToShortCut(string $toolName, CToolRepository $repo, ToolChain $toolChain): RedirectResponse
586
     * {
587
     * $tool = $repo->findOneBy([
588
     * 'name' => $toolName,
589
     * ]);
590
     * if (null === $tool) {
591
     * throw new NotFoundHttpException($this->trans('Tool not found'));
592
     * }
593
     * $tool = $toolChain->getToolFromName($tool->getTool()->getTitle());
594
     * $link = $tool->getLink();
595
     * if (strpos($link, 'nodeId')) {
596
     * $nodeId = (string) $this->getCourse()->getResourceNode()->getId();
597
     * $link = str_replace(':nodeId', $nodeId, $link);
598
     * }
599
     * $url = $link.'?'.$this->getCourseUrlQuery();
600
     * return $this->redirect($url);
601
     * }*/
602
603
    /**
604
     * Edit configuration with given namespace.
605
     */
606
    #[Route('/{course}/settings/{namespace}', name: 'chamilo_core_course_settings')]
607
    public function updateSettings(
608
        Request $request,
609
        #[MapEntity(expr: 'repository.find(cid)')]
610
        Course $course,
611
        string $namespace,
612
        SettingsCourseManager $manager,
613
        SettingsFormFactory $formFactory
614
    ): Response {
615
        $this->denyAccessUnlessGranted(CourseVoter::VIEW, $course);
616
617
        $schemaAlias = $manager->convertNameSpaceToService($namespace);
618
        $settings = $manager->load($namespace);
619
620
        $form = $formFactory->create($schemaAlias);
621
622
        $form->setData($settings);
623
        $form->handleRequest($request);
624
625
        if ($form->isSubmitted() && $form->isValid()) {
626
            $messageType = 'success';
627
628
            try {
629
                $manager->setCourse($course);
630
                $manager->save($form->getData());
631
                $message = $this->trans('Update');
632
            } catch (ValidatorException $validatorException) {
633
                $message = $this->trans($validatorException->getMessage());
634
                $messageType = 'error';
635
            }
636
            $this->addFlash($messageType, $message);
637
638
            if ($request->headers->has('referer')) {
639
                return $this->redirect($request->headers->get('referer'));
640
            }
641
        }
642
643
        $schemas = $manager->getSchemas();
644
645
        return $this->render(
646
            '@ChamiloCore/Course/settings.html.twig',
647
            [
648
                'course' => $course,
649
                'schemas' => $schemas,
650
                'settings' => $settings,
651
                'form' => $form,
652
            ]
653
        );
654
    }
655
656
    #[Route('/{id}/about', name: 'chamilo_core_course_about')]
657
    public function about(
658
        Course $course,
659
        IllustrationRepository $illustrationRepository,
660
        CCourseDescriptionRepository $courseDescriptionRepository,
661
        EntityManagerInterface $em,
662
        Request $request
663
    ): Response {
664
        $courseId = $course->getId();
665
666
        $user = $this->userHelper->getCurrent();
667
668
        $fieldsRepo = $em->getRepository(ExtraField::class);
669
670
        /** @var TagRepository $tagRepo */
671
        $tagRepo = $em->getRepository(Tag::class);
672
673
        $courseDescriptions = $courseDescriptionRepository->getResourcesByCourse($course)->getQuery()->getResult();
674
675
        $courseValues = new ExtraFieldValue('course');
676
677
        $urlCourse = api_get_path(WEB_PATH).\sprintf('course/%s/about', $courseId);
678
        $courseTeachers = $course->getTeachersSubscriptions();
679
        $teachersData = [];
680
681
        foreach ($courseTeachers as $teacherSubscription) {
682
            $teacher = $teacherSubscription->getUser();
683
            $userData = [
684
                'complete_name' => UserManager::formatUserFullName($teacher),
685
                'image' => $illustrationRepository->getIllustrationUrl($teacher),
686
                'diploma' => $teacher->getDiplomas(),
687
                'openarea' => $teacher->getOpenarea(),
688
            ];
689
690
            $teachersData[] = $userData;
691
        }
692
693
        /** @var ExtraField $tagField */
694
        $tagField = $fieldsRepo->findOneBy([
695
            'itemType' => ExtraField::COURSE_FIELD_TYPE,
696
            'variable' => 'tags',
697
        ]);
698
699
        $courseTags = [];
700
        if (null !== $tagField) {
701
            $courseTags = $tagRepo->getTagsByItem($tagField, $courseId);
702
        }
703
704
        $courseDescription = $courseObjectives = $courseTopics = $courseMethodology = '';
705
        $courseMaterial = $courseResources = $courseAssessment = '';
706
        $courseCustom = [];
707
        foreach ($courseDescriptions as $descriptionTool) {
708
            switch ($descriptionTool->getDescriptionType()) {
709
                case CCourseDescription::TYPE_DESCRIPTION:
710
                    $courseDescription = $descriptionTool->getContent();
711
712
                    break;
713
714
                case CCourseDescription::TYPE_OBJECTIVES:
715
                    $courseObjectives = $descriptionTool;
716
717
                    break;
718
719
                case CCourseDescription::TYPE_TOPICS:
720
                    $courseTopics = $descriptionTool;
721
722
                    break;
723
724
                case CCourseDescription::TYPE_METHODOLOGY:
725
                    $courseMethodology = $descriptionTool;
726
727
                    break;
728
729
                case CCourseDescription::TYPE_COURSE_MATERIAL:
730
                    $courseMaterial = $descriptionTool;
731
732
                    break;
733
734
                case CCourseDescription::TYPE_RESOURCES:
735
                    $courseResources = $descriptionTool;
736
737
                    break;
738
739
                case CCourseDescription::TYPE_ASSESSMENT:
740
                    $courseAssessment = $descriptionTool;
741
742
                    break;
743
744
                case CCourseDescription::TYPE_CUSTOM:
745
                    $courseCustom[] = $descriptionTool;
746
747
                    break;
748
            }
749
        }
750
751
        $topics = [
752
            'objectives' => $courseObjectives,
753
            'topics' => $courseTopics,
754
            'methodology' => $courseMethodology,
755
            'material' => $courseMaterial,
756
            'resources' => $courseResources,
757
            'assessment' => $courseAssessment,
758
            'custom' => array_reverse($courseCustom),
759
        ];
760
761
        $subscriptionUser = false;
762
763
        if ($user) {
764
            $subscriptionUser = CourseManager::is_user_subscribed_in_course($user->getId(), $course->getCode());
765
        }
766
767
        $allowSubscribe = CourseManager::canUserSubscribeToCourse($course->getCode());
768
769
        $image = Container::getIllustrationRepository()->getIllustrationUrl($course, 'course_picture_medium');
770
771
        $params = [
772
            'course' => $course,
773
            'description' => $courseDescription,
774
            'image' => $image,
775
            'syllabus' => $topics,
776
            'tags' => $courseTags,
777
            'teachers' => $teachersData,
778
            'extra_fields' => $courseValues->getAllValuesForAnItem(
779
                $course->getId(),
780
                null,
781
                true
782
            ),
783
            'subscription' => $subscriptionUser,
784
            'url' => '',
785
            'is_premium' => '',
786
            'token' => '',
787
            'base_url' => $request->getSchemeAndHttpHost(),
788
            'allow_subscribe' => $allowSubscribe,
789
        ];
790
791
        $metaInfo = '<meta property="og:url" content="'.$urlCourse.'" />';
792
        $metaInfo .= '<meta property="og:type" content="website" />';
793
        $metaInfo .= '<meta property="og:title" content="'.$course->getTitle().'" />';
794
        $metaInfo .= '<meta property="og:description" content="'.strip_tags($courseDescription).'" />';
795
        $metaInfo .= '<meta property="og:image" content="'.$image.'" />';
796
797
        $htmlHeadXtra[] = $metaInfo;
798
        $htmlHeadXtra[] = api_get_asset('readmore-js/readmore.js');
799
800
        return $this->render('@ChamiloCore/Course/about.html.twig', $params);
801
    }
802
803
    #[Route('/{id}/welcome', name: 'chamilo_core_course_welcome')]
804
    public function welcome(Course $course): Response
805
    {
806
        return $this->render('@ChamiloCore/Course/welcome.html.twig', [
807
            'course' => $course,
808
        ]);
809
    }
810
811
    private function findIntroOfCourse(Course $course): ?CTool
812
    {
813
        $qb = $this->em->createQueryBuilder();
814
815
        $query = $qb->select('ct')
816
            ->from(CTool::class, 'ct')
817
            ->where('ct.course = :c_id')
818
            ->andWhere('ct.title = :title')
819
            ->andWhere(
820
                $qb->expr()->orX(
821
                    $qb->expr()->eq('ct.session', ':session_id'),
822
                    $qb->expr()->isNull('ct.session')
823
                )
824
            )
825
            ->setParameters([
826
                'c_id' => $course->getId(),
827
                'title' => 'course_homepage',
828
                'session_id' => 0,
829
            ])
830
            ->getQuery()
831
        ;
832
833
        $results = $query->getResult();
834
835
        return \count($results) > 0 ? $results[0] : null;
836
    }
837
838
    #[Route('/{id}/getToolIntro', name: 'chamilo_core_course_gettoolintro')]
839
    public function getToolIntro(Request $request, Course $course, EntityManagerInterface $em): Response
840
    {
841
        $sessionId = (int) $request->get('sid');
842
843
        // $session = $this->getSession();
844
        $responseData = [];
845
        $ctoolRepo = $em->getRepository(CTool::class);
846
        $sessionRepo = $em->getRepository(Session::class);
847
        $createInSession = false;
848
849
        $session = null;
850
851
        if (!empty($sessionId)) {
852
            $session = $sessionRepo->find($sessionId);
853
        }
854
855
        $ctool = $this->findIntroOfCourse($course);
856
857
        if ($session) {
858
            $ctoolSession = $ctoolRepo->findOneBy(['title' => 'course_homepage', 'course' => $course, 'session' => $session]);
859
860
            if (!$ctoolSession) {
861
                $createInSession = true;
862
            } else {
863
                $ctool = $ctoolSession;
864
            }
865
        }
866
867
        if ($ctool) {
868
            $ctoolintroRepo = $em->getRepository(CToolIntro::class);
869
870
            /** @var CToolIntro $ctoolintro */
871
            $ctoolintro = $ctoolintroRepo->findOneBy(['courseTool' => $ctool]);
872
            if ($ctoolintro) {
873
                $responseData = [
874
                    'iid' => $ctoolintro->getIid(),
875
                    'introText' => $ctoolintro->getIntroText(),
876
                    'createInSession' => $createInSession,
877
                    'cToolId' => $ctool->getIid(),
878
                ];
879
            }
880
            $responseData['c_tool'] = [
881
                'iid' => $ctool->getIid(),
882
                'title' => $ctool->getTitle(),
883
            ];
884
        }
885
886
        return new JsonResponse($responseData);
887
    }
888
889
    #[Route('/{id}/addToolIntro', name: 'chamilo_core_course_addtoolintro')]
890
    public function addToolIntro(Request $request, Course $course, EntityManagerInterface $em): Response
891
    {
892
        $data = json_decode($request->getContent());
893
        $sessionId = $data->sid ?? ($data->resourceLinkList[0]->sid ?? 0);
894
        $introText = $data->introText ?? null;
895
896
        $session = $sessionId ? $em->getRepository(Session::class)->find($sessionId) : null;
897
        $ctoolRepo = $em->getRepository(CTool::class);
898
        $ctoolintroRepo = $em->getRepository(CToolIntro::class);
899
900
        $ctoolSession = $ctoolRepo->findOneBy([
901
            'title' => 'course_homepage',
902
            'course' => $course,
903
            'session' => $session,
904
        ]);
905
906
        if (!$ctoolSession) {
907
            $toolEntity = $em->getRepository(Tool::class)->findOneBy(['title' => 'course_homepage']);
908
            if ($toolEntity) {
909
                $ctoolSession = (new CTool())
910
                    ->setTool($toolEntity)
911
                    ->setTitle('course_homepage')
912
                    ->setCourse($course)
913
                    ->setPosition(1)
914
                    ->setVisibility(true)
915
                    ->setParent($course)
916
                    ->setCreator($course->getCreator())
917
                    ->setSession($session)
918
                    ->addCourseLink($course)
919
                ;
920
921
                $em->persist($ctoolSession);
922
                $em->flush();
923
            }
924
        }
925
926
        $ctoolIntro = $ctoolintroRepo->findOneBy(['courseTool' => $ctoolSession]);
927
        if (!$ctoolIntro) {
928
            $ctoolIntro = (new CToolIntro())
929
                ->setCourseTool($ctoolSession)
930
                ->setIntroText($introText ?? '')
931
                ->setParent($course)
932
            ;
933
934
            $em->persist($ctoolIntro);
935
            $em->flush();
936
937
            return new JsonResponse([
938
                'status' => 'created',
939
                'cToolId' => $ctoolSession->getIid(),
940
                'introIid' => $ctoolIntro->getIid(),
941
                'introText' => $ctoolIntro->getIntroText(),
942
            ]);
943
        }
944
945
        if (null !== $introText) {
946
            $ctoolIntro->setIntroText($introText);
947
            $em->persist($ctoolIntro);
948
            $em->flush();
949
950
            return new JsonResponse([
951
                'status' => 'updated',
952
                'cToolId' => $ctoolSession->getIid(),
953
                'introIid' => $ctoolIntro->getIid(),
954
                'introText' => $ctoolIntro->getIntroText(),
955
            ]);
956
        }
957
958
        return new JsonResponse(['status' => 'no_action']);
959
    }
960
961
    #[Route('/check-enrollments', name: 'chamilo_core_check_enrollments', methods: ['GET'])]
962
    public function checkEnrollments(EntityManagerInterface $em, SettingsManager $settingsManager): JsonResponse
963
    {
964
        $user = $this->userHelper->getCurrent();
965
966
        if (!$user) {
967
            return new JsonResponse(['error' => 'User not found'], Response::HTTP_UNAUTHORIZED);
968
        }
969
970
        $isEnrolledInCourses = $this->isUserEnrolledInAnyCourse($user, $em);
971
        $isEnrolledInSessions = $this->isUserEnrolledInAnySession($user, $em);
972
973
        if (!$isEnrolledInCourses && !$isEnrolledInSessions) {
974
            $defaultMenuEntry = $settingsManager->getSetting('workflows.default_menu_entry_for_course_or_session');
975
            $isEnrolledInCourses = 'my_courses' === $defaultMenuEntry;
976
            $isEnrolledInSessions = 'my_sessions' === $defaultMenuEntry;
977
        }
978
979
        return new JsonResponse([
980
            'isEnrolledInCourses' => $isEnrolledInCourses,
981
            'isEnrolledInSessions' => $isEnrolledInSessions,
982
        ]);
983
    }
984
985
    #[Route('/categories', name: 'chamilo_core_course_form_lists')]
986
    public function getCategories(
987
        SettingsManager $settingsManager,
988
        AccessUrlHelper $accessUrlHelper,
989
        CourseCategoryRepository $courseCategoriesRepo
990
    ): JsonResponse {
991
        $allowBaseCourseCategory = 'true' === $settingsManager->getSetting('course.allow_base_course_category');
992
        $accessUrlId = $accessUrlHelper->getCurrent()->getId();
993
994
        $categories = $courseCategoriesRepo->findAllInAccessUrl(
995
            $accessUrlId,
996
            $allowBaseCourseCategory
997
        );
998
999
        $data = [];
1000
        $categoryToAvoid = '';
1001
        if (!$this->isGranted('ROLE_ADMIN')) {
1002
            $categoryToAvoid = $settingsManager->getSetting('course.course_category_code_to_use_as_model');
1003
        }
1004
1005
        foreach ($categories as $category) {
1006
            $categoryCode = $category->getCode();
1007
            if (!empty($categoryToAvoid) && $categoryToAvoid == $categoryCode) {
1008
                continue;
1009
            }
1010
            $data[] = ['id' => $category->getId(), 'name' => $category->__toString()];
1011
        }
1012
1013
        return new JsonResponse($data);
1014
    }
1015
1016
    #[Route('/search_templates', name: 'chamilo_core_course_search_templates')]
1017
    public function searchCourseTemplates(
1018
        Request $request,
1019
        AccessUrlHelper $accessUrlUtil,
1020
        CourseRepository $courseRepository
1021
    ): JsonResponse {
1022
        $searchTerm = $request->query->get('search', '');
1023
        $accessUrl = $accessUrlUtil->getCurrent();
1024
1025
        $user = $this->userHelper->getCurrent();
1026
1027
        $courseList = $courseRepository->getCoursesInfoByUser($user, $accessUrl, 1, $searchTerm);
1028
        $results = ['items' => []];
1029
        foreach ($courseList as $course) {
1030
            $title = $course['title'];
1031
            $results['items'][] = [
1032
                'id' => $course['id'],
1033
                'name' => $title.' ('.$course['code'].') ',
1034
            ];
1035
        }
1036
1037
        return new JsonResponse($results);
1038
    }
1039
1040
    #[Route('/create', name: 'chamilo_core_course_create')]
1041
    public function createCourse(
1042
        Request $request,
1043
        TranslatorInterface $translator,
1044
        CourseHelper $courseHelper
1045
    ): JsonResponse {
1046
        $courseData = json_decode($request->getContent(), true);
1047
1048
        $title = $courseData['name'] ?? null;
1049
        $wantedCode = $courseData['code'] ?? null;
1050
        $courseLanguage = $courseData['language'] ?? null;
1051
        $categoryCode = $courseData['category'] ?? null;
1052
        $exemplaryContent = $courseData['fillDemoContent'] ?? false;
1053
        $template = $courseData['template'] ?? '';
1054
1055
        $params = [
1056
            'title' => $title,
1057
            'wanted_code' => $wantedCode,
1058
            'course_language' => $courseLanguage,
1059
            'exemplary_content' => $exemplaryContent,
1060
            'course_template' => $template,
1061
        ];
1062
1063
        if ($categoryCode) {
1064
            $params['course_categories'] = $categoryCode;
1065
        }
1066
1067
        try {
1068
            $course = $courseHelper->createCourse($params);
1069
            if ($course) {
1070
                return new JsonResponse([
1071
                    'success' => true,
1072
                    'message' => $translator->trans('Course created successfully.'),
1073
                    'courseId' => $course->getId(),
1074
                ]);
1075
            }
1076
        } catch (Exception $e) {
1077
            return new JsonResponse([
1078
                'success' => false,
1079
                'message' => $translator->trans($e->getMessage()),
1080
            ], Response::HTTP_BAD_REQUEST);
1081
        }
1082
1083
        return new JsonResponse(['success' => false, 'message' => $translator->trans('An error occurred while creating the course.')]);
1084
    }
1085
1086
    #[Route('/{id}/getAutoLaunchExerciseId', name: 'chamilo_core_course_get_auto_launch_exercise_id', methods: ['GET'])]
1087
    public function getAutoLaunchExerciseId(
1088
        Request $request,
1089
        Course $course,
1090
        CQuizRepository $quizRepository,
1091
        EntityManagerInterface $em
1092
    ): JsonResponse {
1093
        $data = $request->getContent();
1094
        $data = json_decode($data);
1095
        $sessionId = $data->sid ?? 0;
1096
1097
        $sessionRepo = $em->getRepository(Session::class);
1098
        $session = null;
1099
        if (!empty($sessionId)) {
1100
            $session = $sessionRepo->find($sessionId);
1101
        }
1102
1103
        $autoLaunchExerciseId = $quizRepository->findAutoLaunchableQuizByCourseAndSession($course, $session);
1104
1105
        return new JsonResponse(['exerciseId' => $autoLaunchExerciseId], Response::HTTP_OK);
1106
    }
1107
1108
    #[Route('/{id}/getAutoLaunchLPId', name: 'chamilo_core_course_get_auto_launch_lp_id', methods: ['GET'])]
1109
    public function getAutoLaunchLPId(
1110
        Request $request,
1111
        Course $course,
1112
        CLpRepository $lpRepository,
1113
        EntityManagerInterface $em
1114
    ): JsonResponse {
1115
        $data = $request->getContent();
1116
        $data = json_decode($data);
1117
        $sessionId = $data->sid ?? 0;
1118
1119
        $sessionRepo = $em->getRepository(Session::class);
1120
        $session = null;
1121
        if (!empty($sessionId)) {
1122
            $session = $sessionRepo->find($sessionId);
1123
        }
1124
1125
        $autoLaunchLPId = $lpRepository->findAutoLaunchableLPByCourseAndSession($course, $session);
1126
1127
        return new JsonResponse(['lpId' => $autoLaunchLPId], Response::HTTP_OK);
1128
    }
1129
1130
    private function autoLaunch(): void
1131
    {
1132
        $autoLaunchWarning = '';
1133
        $showAutoLaunchLpWarning = false;
1134
        $course_id = api_get_course_int_id();
1135
        $lpAutoLaunch = api_get_course_setting('enable_lp_auto_launch');
1136
        $session_id = api_get_session_id();
1137
        $allowAutoLaunchForCourseAdmins =
1138
            api_is_platform_admin()
1139
            || api_is_allowed_to_edit(true, true)
1140
            || api_is_coach();
1141
1142
        if (!empty($lpAutoLaunch)) {
1143
            if (2 === $lpAutoLaunch) {
1144
                // LP list
1145
                if ($allowAutoLaunchForCourseAdmins) {
1146
                    $showAutoLaunchLpWarning = true;
1147
                } else {
1148
                    $session_key = 'lp_autolaunch_'.$session_id.'_'.$course_id.'_'.api_get_user_id();
1149
                    if (!isset($_SESSION[$session_key])) {
1150
                        // Redirecting to the LP
1151
                        $url = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?'.api_get_cidreq();
1152
                        $_SESSION[$session_key] = true;
1153
                        header(\sprintf('Location: %s', $url));
1154
1155
                        exit;
1156
                    }
1157
                }
1158
            } else {
1159
                $lp_table = Database::get_course_table(TABLE_LP_MAIN);
1160
                $condition = '';
1161
                if (!empty($session_id)) {
1162
                    $condition = api_get_session_condition($session_id);
1163
                    $sql = "SELECT id FROM {$lp_table}
1164
                            WHERE c_id = {$course_id} AND autolaunch = 1 {$condition}
1165
                            LIMIT 1";
1166
                    $result = Database::query($sql);
1167
                    // If we found nothing in the session we just called the session_id =  0 autolaunch
1168
                    if (0 === Database::num_rows($result)) {
1169
                        $condition = '';
1170
                    }
1171
                }
1172
1173
                $sql = "SELECT iid FROM {$lp_table}
1174
                        WHERE c_id = {$course_id} AND autolaunch = 1 {$condition}
1175
                        LIMIT 1";
1176
                $result = Database::query($sql);
1177
                if (Database::num_rows($result) > 0) {
1178
                    $lp_data = Database::fetch_array($result);
1179
                    if (!empty($lp_data['iid'])) {
1180
                        if ($allowAutoLaunchForCourseAdmins) {
1181
                            $showAutoLaunchLpWarning = true;
1182
                        } else {
1183
                            $session_key = 'lp_autolaunch_'.$session_id.'_'.api_get_course_int_id().'_'.api_get_user_id();
1184
                            if (!isset($_SESSION[$session_key])) {
1185
                                // Redirecting to the LP
1186
                                $url = api_get_path(WEB_CODE_PATH).
1187
                                    'lp/lp_controller.php?'.api_get_cidreq().'&action=view&lp_id='.$lp_data['iid'];
1188
1189
                                $_SESSION[$session_key] = true;
1190
                                header(\sprintf('Location: %s', $url));
1191
1192
                                exit;
1193
                            }
1194
                        }
1195
                    }
1196
                }
1197
            }
1198
        }
1199
1200
        if ($showAutoLaunchLpWarning) {
1201
            $autoLaunchWarning = get_lang(
1202
                'The learning path auto-launch setting is ON. When learners enter this course, they will be automatically redirected to the learning path marked as auto-launch.'
1203
            );
1204
        }
1205
1206
        $forumAutoLaunch = (int) api_get_course_setting('enable_forum_auto_launch');
1207
        if (1 === $forumAutoLaunch) {
1208
            if ($allowAutoLaunchForCourseAdmins) {
1209
                if (empty($autoLaunchWarning)) {
1210
                    $autoLaunchWarning = get_lang(
1211
                        "The forum's auto-launch setting is on. Students will be redirected to the forum tool when entering this course."
1212
                    );
1213
                }
1214
            } else {
1215
                $url = api_get_path(WEB_CODE_PATH).'forum/index.php?'.api_get_cidreq();
1216
                header(\sprintf('Location: %s', $url));
1217
1218
                exit;
1219
            }
1220
        }
1221
1222
        $exerciseAutoLaunch = (int) api_get_course_setting('enable_exercise_auto_launch');
1223
        if (2 === $exerciseAutoLaunch) {
1224
            if ($allowAutoLaunchForCourseAdmins) {
1225
                if (empty($autoLaunchWarning)) {
1226
                    $autoLaunchWarning = get_lang(
1227
                        'TheExerciseAutoLaunchSettingIsONStudentsWillBeRedirectToTheExerciseList'
1228
                    );
1229
                }
1230
            } else {
1231
                // Redirecting to the document
1232
                $url = api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq();
1233
                header(\sprintf('Location: %s', $url));
1234
1235
                exit;
1236
            }
1237
        } elseif (1 === $exerciseAutoLaunch) {
1238
            if ($allowAutoLaunchForCourseAdmins) {
1239
                if (empty($autoLaunchWarning)) {
1240
                    $autoLaunchWarning = get_lang(
1241
                        'TheExerciseAutoLaunchSettingIsONStudentsWillBeRedirectToAnSpecificExercise'
1242
                    );
1243
                }
1244
            } else {
1245
                // Redirecting to an exercise
1246
                $table = Database::get_course_table(TABLE_QUIZ_TEST);
1247
                $condition = '';
1248
                if (!empty($session_id)) {
1249
                    $condition = api_get_session_condition($session_id);
1250
                    $sql = "SELECT iid FROM {$table}
1251
                            WHERE c_id = {$course_id} AND autolaunch = 1 {$condition}
1252
                            LIMIT 1";
1253
                    $result = Database::query($sql);
1254
                    // If we found nothing in the session we just called the session_id = 0 autolaunch
1255
                    if (0 === Database::num_rows($result)) {
1256
                        $condition = '';
1257
                    }
1258
                }
1259
1260
                $sql = "SELECT iid FROM {$table}
1261
                        WHERE c_id = {$course_id} AND autolaunch = 1 {$condition}
1262
                        LIMIT 1";
1263
                $result = Database::query($sql);
1264
                if (Database::num_rows($result) > 0) {
1265
                    $row = Database::fetch_array($result);
1266
                    $exerciseId = $row['iid'];
1267
                    $url = api_get_path(WEB_CODE_PATH).
1268
                        'exercise/overview.php?exerciseId='.$exerciseId.'&'.api_get_cidreq();
1269
                    header(\sprintf('Location: %s', $url));
1270
1271
                    exit;
1272
                }
1273
            }
1274
        }
1275
1276
        $documentAutoLaunch = (int) api_get_course_setting('enable_document_auto_launch');
1277
        if (1 === $documentAutoLaunch) {
1278
            if ($allowAutoLaunchForCourseAdmins) {
1279
                if (empty($autoLaunchWarning)) {
1280
                    $autoLaunchWarning = get_lang(
1281
                        'The document auto-launch feature configuration is enabled. Learners will be automatically redirected to document tool.'
1282
                    );
1283
                }
1284
            } else {
1285
                // Redirecting to the document
1286
                $url = api_get_path(WEB_CODE_PATH).'document/document.php?'.api_get_cidreq();
1287
                header("Location: $url");
1288
1289
                exit;
1290
            }
1291
        }
1292
1293
        /*  SWITCH TO A DIFFERENT HOMEPAGE VIEW
1294
         the setting homepage_view is adjustable through
1295
         the platform administration section */
1296
        if (!empty($autoLaunchWarning)) {
1297
            $this->addFlash(
1298
                'warning',
1299
                Display::return_message(
1300
                    $autoLaunchWarning,
1301
                    'warning'
1302
                )
1303
            );
1304
        }
1305
    }
1306
1307
    // Implement the real logic to check course enrollment
1308
    private function isUserEnrolledInAnyCourse(User $user, EntityManagerInterface $em): bool
1309
    {
1310
        $enrollmentCount = $em
1311
            ->getRepository(CourseRelUser::class)
1312
            ->count(['user' => $user])
1313
        ;
1314
1315
        return $enrollmentCount > 0;
1316
    }
1317
1318
    // Implement the real logic to check session enrollment
1319
    private function isUserEnrolledInAnySession(User $user, EntityManagerInterface $em): bool
1320
    {
1321
        $enrollmentCount = $em->getRepository(SessionRelUser::class)
1322
            ->count(['user' => $user])
1323
        ;
1324
1325
        return $enrollmentCount > 0;
1326
    }
1327
1328
    /**
1329
     * Reorders all course tools for a given course / session after drag & drop.
1330
     *
1331
     * @return array<string, mixed>
1332
     */
1333
    private function reorderCourseTools(
1334
        EntityManagerInterface $em,
1335
        Course $course,
1336
        int $sessionId,
1337
        int $toolId,
1338
        int $newIndex
1339
    ): array {
1340
        if ($toolId <= 0) {
1341
            return [
1342
                'success' => false,
1343
                'message' => 'Invalid tool id.',
1344
            ];
1345
        }
1346
1347
        /** @var CToolRepository $toolRepo */
1348
        $toolRepo = $em->getRepository(CTool::class);
1349
1350
        // Load all tools for this course + (optional) session ordered by current position
1351
        $qb = $toolRepo->createQueryBuilder('t')
1352
            ->andWhere('t.course = :course')
1353
            ->setParameter('course', $course)
1354
            ->orderBy('t.position', 'ASC')
1355
            ->addOrderBy('t.iid', 'ASC');
1356
1357
        if ($sessionId > 0) {
1358
            $qb
1359
                ->andWhere('IDENTITY(t.session) = :sessionId')
1360
                ->setParameter('sessionId', $sessionId);
1361
        } else {
1362
            $qb->andWhere('t.session IS NULL');
1363
        }
1364
1365
        /** @var CTool[] $tools */
1366
        $tools = $qb->getQuery()->getResult();
1367
1368
        if (0 === \count($tools)) {
1369
            return [
1370
                'success' => false,
1371
                'message' => 'No tools found for course / session.',
1372
            ];
1373
        }
1374
1375
        // Build an array of IDs to manipulate positions easily
1376
        $ids = array_map(static fn (CTool $tool): int => $tool->getIid() ?? 0, $tools);
1377
1378
        $currentIndex = array_search($toolId, $ids, true);
1379
        if (false === $currentIndex) {
1380
            return [
1381
                'success' => false,
1382
                'message' => 'Tool not found in current course / session.',
1383
            ];
1384
        }
1385
1386
        // Clamp the new index into a valid range
1387
        $newIndex = max(0, min($newIndex, \count($tools) - 1));
1388
1389
        if ($newIndex === $currentIndex) {
1390
            return [
1391
                'success' => true,
1392
                'unchanged' => true,
1393
                'from' => $currentIndex,
1394
                'to' => $newIndex,
1395
                'total' => \count($tools),
1396
            ];
1397
        }
1398
1399
        // Move the ID in the array (remove at old index, insert at new index)
1400
        $idToMove = $ids[$currentIndex];
1401
        array_splice($ids, $currentIndex, 1);
1402
        array_splice($ids, $newIndex, 0, [$idToMove]);
1403
1404
        // Rewrite all positions in DB using a simple DQL UPDATE per tool
1405
        // Positions will be 0-based: 0,1,2,...
1406
        foreach ($ids as $pos => $id) {
1407
            $em->createQueryBuilder()
1408
                ->update(CTool::class, 't')
1409
                ->set('t.position', ':pos')
1410
                ->where('t.iid = :iid')
1411
                ->setParameter('pos', $pos)
1412
                ->setParameter('iid', $id)
1413
                ->getQuery()
1414
                ->execute();
1415
        }
1416
1417
        return [
1418
            'success' => true,
1419
            'from' => $currentIndex,
1420
            'to' => $newIndex,
1421
            'total' => \count($tools),
1422
            'courseId' => $course->getId(),
1423
            'sessionId' => $sessionId,
1424
        ];
1425
    }
1426
}
1427