Passed
Push — master ( 5e311d...0c57ab )
by
unknown
17:52 queued 08:41
created

CourseController::getAutoLaunchLPId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
c 0
b 0
f 0
nc 2
nop 4
dl 0
loc 20
rs 9.9332
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 Doctrine\Persistence\ManagerRegistry;
57
use Event;
58
use Exception;
59
use Exercise;
60
use ExtraFieldValue;
61
use Graphp\GraphViz\GraphViz;
62
use IntlDateFormatter;
63
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
64
use Symfony\Bundle\SecurityBundle\Security;
65
use Symfony\Component\HttpFoundation\JsonResponse;
66
use Symfony\Component\HttpFoundation\RedirectResponse;
67
use Symfony\Component\HttpFoundation\Request;
68
use Symfony\Component\HttpFoundation\Response;
69
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
70
use Symfony\Component\Routing\Attribute\Route;
71
use Symfony\Component\Security\Http\Attribute\IsGranted;
72
use Symfony\Component\Serializer\SerializerInterface;
73
use Symfony\Component\Validator\Exception\ValidatorException;
74
use Symfony\Contracts\Translation\TranslatorInterface;
75
use UserManager;
76
77
/**
78
 * @author Julio Montoya <[email protected]>
79
 */
80
#[Route('/course')]
81
class CourseController extends ToolBaseController
82
{
83
    public function __construct(
84
        private readonly EntityManagerInterface $em,
85
        private readonly SerializerInterface $serializer,
86
        private readonly UserHelper $userHelper,
87
        private readonly ManagerRegistry $doctrine,
88
    ) {}
89
90
    /**
91
     * Extend the controller service locator to include "doctrine".
92
     * This prevents runtime errors when legacy code tries to access it through the controller container.
93
     */
94
    public static function getSubscribedServices(): array
95
    {
96
        return array_merge(parent::getSubscribedServices(), [
97
            'doctrine' => ManagerRegistry::class,
98
        ]);
99
    }
100
101
    #[IsGranted('ROLE_USER')]
102
    #[Route('/{cid}/checkLegal.json', name: 'chamilo_core_course_check_legal_json')]
103
    public function checkTermsAndConditionJson(
104
        Request $request,
105
        LegalRepository $legalTermsRepo,
106
        LanguageRepository $languageRepository,
107
        ExtraFieldValuesRepository $extraFieldValuesRepository,
108
        SettingsManager $settingsManager
109
    ): Response {
110
        $user = $this->userHelper->getCurrent();
111
        $course = $this->getCourse();
112
        $sid = $this->getSessionId();
113
114
        $responseData = [
115
            'redirect' => false,
116
            'url' => '#',
117
        ];
118
119
        if ($user->isStudent()
120
            && 'true' === $settingsManager->getSetting('registration.allow_terms_conditions', true)
121
            && 'course' === $settingsManager->getSetting('workflows.load_term_conditions_section', true)
122
        ) {
123
            $termAndConditionStatus = false;
124
            $extraValue = $extraFieldValuesRepository->findLegalAcceptByItemId($user->getId());
125
            if (!empty($extraValue['value'])) {
126
                $result = $extraValue['value'];
127
                $userConditions = explode(':', $result);
128
                $version = $userConditions[0];
129
                $langId = (int) $userConditions[1];
130
                $realVersion = $legalTermsRepo->getLastVersion($langId);
131
                $termAndConditionStatus = ($version >= $realVersion);
132
            }
133
134
            if (false === $termAndConditionStatus) {
135
                $request->getSession()->set('term_and_condition', ['user_id' => $user->getId()]);
136
137
                $redirect = true;
138
139
                if ('true' === $settingsManager->getSetting('course.allow_public_course_with_no_terms_conditions', true)
140
                    && Course::OPEN_WORLD === $course->getVisibility()
141
                ) {
142
                    $redirect = false;
143
                }
144
145
                if ($redirect && !$this->isGranted('ROLE_ADMIN')) {
146
                    $request->getSession()->remove('cid');
147
                    $request->getSession()->remove('course');
148
149
                    // Build return URL
150
                    $returnUrl = '/course/'.$course->getId().'/home?sid='.$sid;
151
152
                    $responseData = [
153
                        'redirect' => true,
154
                        'url' => '/main/auth/tc.php?return='.urlencode($returnUrl),
155
                    ];
156
                }
157
            } else {
158
                $request->getSession()->remove('term_and_condition');
159
            }
160
        }
161
162
        return new JsonResponse($responseData);
163
    }
164
165
    #[Route('/{cid}/home.json', name: 'chamilo_core_course_home_json')]
166
    public function indexJson(
167
        Request $request,
168
        CShortcutRepository $shortcutRepository,
169
        EntityManagerInterface $em,
170
        AssetRepository $assetRepository
171
    ): Response {
172
        // Handle drag & drop sort for course tools
173
        if ($request->isMethod('POST')) {
174
            $requestData = json_decode($request->getContent() ?: '', true) ?? [];
175
176
            if (isset($requestData['toolId'], $requestData['index'])) {
177
                $course = $this->getCourse();
178
                if (null === $course) {
179
                    return $this->json(
180
                        ['success' => false, 'message' => 'Course not found.'],
181
                        Response::HTTP_BAD_REQUEST
182
                    );
183
                }
184
185
                $sessionId = $this->getSessionId();
186
                $toolId = (int) $requestData['toolId'];
187
                $newIndex = (int) $requestData['index'];
188
189
                $result = $this->reorderCourseTools($em, $course, $sessionId, $toolId, $newIndex);
190
                $statusCode = $result['success'] ? Response::HTTP_OK : Response::HTTP_BAD_REQUEST;
191
192
                return $this->json($result, $statusCode);
193
            }
194
        }
195
196
        $course = $this->getCourse();
197
        $sessionId = $this->getSessionId();
198
        $isInASession = $sessionId > 0;
199
200
        if (null === $course) {
201
            throw $this->createAccessDeniedException();
202
        }
203
204
        if (empty($sessionId)) {
205
            $this->denyAccessUnlessGranted(CourseVoter::VIEW, $course);
206
        }
207
208
        $sessionHandler = $request->getSession();
209
210
        $userId = 0;
211
212
        $user = $this->userHelper->getCurrent();
213
        if (null !== $user) {
214
            $userId = $user->getId();
215
        }
216
217
        $courseCode = $course->getCode();
218
        $courseId = $course->getId();
219
220
        if ($user && $user->isInvitee()) {
221
            $isSubscribed = CourseManager::is_user_subscribed_in_course(
222
                $userId,
223
                $courseCode,
224
                $isInASession,
225
                $sessionId
226
            );
227
228
            if (!$isSubscribed) {
229
                throw $this->createAccessDeniedException();
230
            }
231
        }
232
233
        $isSpecialCourse = CourseManager::isSpecialCourse($courseId);
234
235
        if ($user && $isSpecialCourse && (isset($_GET['autoreg']) && 1 === (int) $_GET['autoreg'])
236
            && CourseManager::subscribeUser($userId, $courseId, STUDENT)
237
        ) {
238
            $sessionHandler->set('is_allowed_in_course', true);
239
        }
240
241
        $logInfo = [
242
            'tool' => 'course-main',
243
        ];
244
        Event::registerLog($logInfo);
245
246
        // Deleting the objects
247
        $sessionHandler->remove('toolgroup');
248
        $sessionHandler->remove('_gid');
249
        $sessionHandler->remove('oLP');
250
        $sessionHandler->remove('lpobject');
251
252
        api_remove_in_gradebook();
253
        Exercise::cleanSessionVariables();
254
255
        $shortcuts = [];
256
        if (null !== $user) {
257
            $shortcutQuery = $shortcutRepository->getResources($course->getResourceNode());
258
            $shortcuts = $shortcutQuery->getQuery()->getResult();
259
260
            $courseNodeId = $course->getResourceNode()->getId();
261
            $cid = $course->getId();
262
            $sid = $this->getSessionId() ?: null;
263
264
            /** @var CShortcut $shortcut */
265
            /** @var CShortcut $shortcut */
266
            foreach ($shortcuts as $shortcut) {
267
                $resourceNode = $shortcut->getShortCutNode();
268
269
                // Try as CLink
270
                $cLink = $em->getRepository(CLink::class)->findOneBy(['resourceNode' => $resourceNode]);
271
                if ($cLink) {
272
                    // Image (if any)
273
                    $shortcut->setCustomImageUrl(
274
                        $cLink->getCustomImage()
275
                            ? $assetRepository->getAssetUrl($cLink->getCustomImage())
276
                            : null
277
                    );
278
279
                    // External link behavior
280
                    $shortcut->setUrlOverride($cLink->getUrl()); // open external URL
281
                    $shortcut->setIcon(null);                    // keep default icon for links
282
                    $shortcut->target = $cLink->getTarget();     // e.g. "_blank"
283
284
                    continue;
285
                }
286
287
                // Try as CBlog
288
                $cBlog = $em->getRepository(CBlog::class)
289
                    ->findOneBy(['resourceNode' => $resourceNode])
290
                ;
291
292
                if ($cBlog) {
293
                    $courseNodeId = $course->getResourceNode()->getId();
294
                    $cid = $course->getId();
295
                    $sid = $this->getSessionId() ?: null;
296
297
                    $qs = http_build_query(array_filter([
298
                        'cid' => $cid,
299
                        'sid' => $sid ?: null,
300
                        'gid' => 0,
301
                    ], static fn ($v) => null !== $v));
302
303
                    $shortcut->setUrlOverride(\sprintf(
304
                        '/resources/blog/%d/%d/posts?%s',
305
                        $courseNodeId,
306
                        $cBlog->getIid(),
307
                        $qs
308
                    ));
309
                    $shortcut->setIcon('mdi-notebook-outline');  // blog icon
310
                    $shortcut->setCustomImageUrl(null);          // blogs use icon by default
311
                    $shortcut->target = '_self';
312
313
                    continue;
314
                }
315
316
                // Fallback
317
                $shortcut->setCustomImageUrl(null);
318
                $shortcut->setUrlOverride(null);
319
                $shortcut->setIcon(null);
320
                $shortcut->target = '_self';
321
            }
322
        }
323
        $responseData = [
324
            'shortcuts' => $shortcuts,
325
            'diagram' => '',
326
        ];
327
328
        $json = $this->serializer->serialize(
329
            $responseData,
330
            'json',
331
            [
332
                'groups' => ['course:read', 'ctool:read', 'tool:read', 'cshortcut:read'],
333
            ]
334
        );
335
336
        return new Response(
337
            $json,
338
            Response::HTTP_OK,
339
            [
340
                'Content-type' => 'application/json',
341
            ]
342
        );
343
    }
344
345
    #[Route('/{cid}/thematic_progress.json', name: 'chamilo_core_course_thematic_progress_json', methods: ['GET'])]
346
    public function thematicProgressJson(
347
        Request $request,
348
        CThematicRepository $thematicRepository,
349
        UserHelper $userHelper,
350
        CidReqHelper $cidReqHelper,
351
        TranslatorInterface $translator,
352
        SettingsCourseManager $courseSettingsManager
353
    ): JsonResponse {
354
        $course = $this->getCourse();
355
        if (null === $course) {
356
            throw $this->createAccessDeniedException();
357
        }
358
359
        if (0 === $this->getSessionId()) {
360
            $this->denyAccessUnlessGranted(CourseVoter::VIEW, $course);
361
        }
362
363
        $courseSettingsManager->setCourse($course);
364
        $displayMode = (string) $courseSettingsManager->getCourseSettingValue('display_info_advance_inside_homecourse');
365
366
        if ('' === $displayMode || '0' === $displayMode || '4' === $displayMode) {
367
            return new JsonResponse(['enabled' => false]);
368
        }
369
370
        $sessionEntity = $cidReqHelper->getSessionEntity();
371
        $currentUser = $userHelper->getCurrent();
372
373
        $advance1 = null;
374
        $advance2 = null;
375
        $subtitle1 = '';
376
        $subtitle2 = '';
377
378
        if ('1' === $displayMode) {
379
            // Last completed topic only
380
            $advance1 = $thematicRepository->findLastDoneAdvanceForCourse($course, $sessionEntity);
381
            if (null !== $advance1) {
382
                $subtitle1 = $translator->trans('Current topic');
383
            }
384
        } elseif ('2' === $displayMode) {
385
            // Two next not done topics
386
            $nextList = $thematicRepository->findNextNotDoneAdvancesForCourse($course, $sessionEntity, 2);
387
388
            if (isset($nextList[0]) && $nextList[0] instanceof CThematicAdvance) {
389
                $advance1 = $nextList[0];
390
                $subtitle1 = $translator->trans('Next topic');
391
            }
392
393
            if (isset($nextList[1]) && $nextList[1] instanceof CThematicAdvance) {
394
                $advance2 = $nextList[1];
395
                $subtitle2 = $translator->trans('Next topic');
396
            }
397
        } elseif ('3' === $displayMode) {
398
            // Current (last done) + next not done
399
            $advance1 = $thematicRepository->findLastDoneAdvanceForCourse($course, $sessionEntity);
400
            $nextList = $thematicRepository->findNextNotDoneAdvancesForCourse($course, $sessionEntity, 1);
401
402
            if (null !== $advance1) {
403
                $subtitle1 = $translator->trans('Current topic');
404
            }
405
406
            if (isset($nextList[0]) && $nextList[0] instanceof CThematicAdvance) {
407
                $advance2 = $nextList[0];
408
                $subtitle2 = $translator->trans('Next topic');
409
            }
410
        } else {
411
            return new JsonResponse(['enabled' => false]);
412
        }
413
414
        if (null === $advance1 && null === $advance2) {
415
            return new JsonResponse(['enabled' => false]);
416
        }
417
418
        $locale = $request->getLocale();
419
        $timezoneId = null;
420
421
        if ($currentUser && method_exists($currentUser, 'getTimezone') && $currentUser->getTimezone()) {
422
            $timezoneId = $currentUser->getTimezone();
423
        }
424
425
        if (empty($timezoneId)) {
426
            $timezoneId = date_default_timezone_get();
427
        }
428
429
        $dateFormatter = new IntlDateFormatter(
430
            $locale,
431
            IntlDateFormatter::MEDIUM,
432
            IntlDateFormatter::SHORT,
433
            $timezoneId
434
        );
435
436
        $buildItem = function (CThematicAdvance $advance, string $type, string $label) use ($dateFormatter): array {
437
            $thematic = $advance->getThematic();
438
439
            $startDate = $advance->getStartDate();
440
            $formattedDate = $startDate instanceof DateTimeInterface
441
                ? (string) $dateFormatter->format($startDate)
442
                : '';
443
444
            return [
445
                'type' => $type,
446
                'label' => $label,
447
                'title' => strip_tags($thematic->getTitle() ?? ''),
448
                'startDate' => $formattedDate,
449
                'content' => strip_tags($advance->getContent() ?? ''),
450
                'duration' => (float) $advance->getDuration(),
451
            ];
452
        };
453
454
        $items = [];
455
456
        if (null !== $advance1) {
457
            $firstType = ('1' === $displayMode || '3' === $displayMode) ? 'current' : 'next';
458
            $items[] = $buildItem($advance1, $firstType, $subtitle1);
459
        }
460
461
        if (null !== $advance2) {
462
            $items[] = $buildItem($advance2, 'next', $subtitle2);
463
        }
464
465
        $userPayload = null;
466
        if ($currentUser) {
467
            $name = method_exists($currentUser, 'getCompleteName')
468
                ? $currentUser->getCompleteName()
469
                : trim(\sprintf('%s %s', $currentUser->getFirstname(), $currentUser->getLastname()));
470
471
            $userPayload = [
472
                'name' => $name,
473
                'avatar' => null,
474
            ];
475
        }
476
477
        $thematicUrl = '/main/course_progress/index.php?cid='.$course->getId().'&sid='.$this->getSessionId().'&action=thematic_details';
478
        $thematicScoreRaw = $thematicRepository->calculateTotalAverageForCourse($course, $sessionEntity);
479
        $thematicScore = $thematicScoreRaw.'%';
480
481
        $payload = [
482
            'enabled' => true,
483
            'displayMode' => (int) $displayMode,
484
            'title' => $translator->trans('Thematic advance'),
485
            'score' => $thematicScore,
486
            'scoreRaw' => $thematicScoreRaw,
487
            'user' => $userPayload,
488
            'items' => $items,
489
            'detailUrl' => $thematicUrl,
490
            'labels' => [
491
                'duration' => $translator->trans('Duration in hours'),
492
                'seeDetail' => $translator->trans('See detail'),
493
            ],
494
        ];
495
496
        return new JsonResponse($payload);
497
    }
498
499
    #[IsGranted('ROLE_USER')]
500
    #[Route('/{courseId}/next-course', name: 'chamilo_course_next_course')]
501
    public function getNextCourse(
502
        int $courseId,
503
        Request $request,
504
        SequenceResourceRepository $repo,
505
        Security $security,
506
        SettingsManager $settingsManager,
507
        EntityManagerInterface $em
508
    ): JsonResponse {
509
        $sessionId = $request->query->getInt('sid');
510
        $useDependents = $request->query->getBoolean('dependents', false);
511
        $user = $security->getUser();
512
513
        if (null === $user || !method_exists($user, 'getId')) {
514
            return new JsonResponse(['error' => 'Authentication required.'], Response::HTTP_UNAUTHORIZED);
515
        }
516
517
        $userId = $user->getId();
518
519
        if ($useDependents) {
520
            $sequences = $repo->getDependents($courseId, SequenceResource::COURSE_TYPE);
521
            $checked = $repo->checkDependentsForUser($sequences, SequenceResource::COURSE_TYPE, $userId, $sessionId, $courseId);
522
            $isUnlocked = $repo->checkSequenceAreCompleted($checked);
523
            $sequenceResource = $repo->findRequirementForResource($courseId, SequenceResource::COURSE_TYPE);
524
        } else {
525
            $sequences = $repo->getRequirements($courseId, SequenceResource::COURSE_TYPE);
526
527
            $hasValidRequirement = false;
528
            foreach ($sequences as $sequence) {
529
                foreach ($sequence['requirements'] ?? [] as $resource) {
530
                    if ($resource instanceof Course) {
531
                        $hasValidRequirement = true;
532
533
                        break 2;
534
                    }
535
                }
536
            }
537
538
            if (!$hasValidRequirement) {
539
                return new JsonResponse([]);
540
            }
541
542
            $checked = $repo->checkRequirementsForUser($sequences, SequenceResource::COURSE_TYPE, $userId, $sessionId);
543
            $isUnlocked = $repo->checkSequenceAreCompleted($checked);
544
            $sequenceResource = $repo->findRequirementForResource($courseId, SequenceResource::COURSE_TYPE);
545
        }
546
547
        $graphImage = null;
548
549
        if ($sequenceResource && $sequenceResource->hasGraph()) {
550
            $graph = $sequenceResource->getSequence()->getUnSerializeGraph();
551
            if (null !== $graph) {
552
                $graph->setAttribute('graphviz.node.fontname', 'arial');
553
                $graphviz = new GraphViz();
554
                $graphImage = $graphviz->createImageSrc($graph);
555
            }
556
        }
557
558
        return new JsonResponse([
559
            'sequenceList' => array_values($checked),
560
            'allowSubscription' => $isUnlocked,
561
            'graph' => $graphImage,
562
        ]);
563
    }
564
565
    /**
566
     * Redirects the page to a tool, following the tools settings.
567
     */
568
    #[Route('/{cid}/tool/{toolName}', name: 'chamilo_core_course_redirect_tool')]
569
    public function redirectTool(
570
        Request $request,
571
        string $toolName,
572
        CToolRepository $repo,
573
        ToolChain $toolChain
574
    ): RedirectResponse {
575
        /** @var CTool|null $tool */
576
        $tool = $repo->findOneBy([
577
            'title' => $toolName,
578
        ]);
579
580
        if (null === $tool) {
581
            throw new NotFoundHttpException($this->trans('Tool not found'));
582
        }
583
584
        $tool = $toolChain->getToolFromName($tool->getTool()->getTitle());
585
        $link = $tool->getLink();
586
587
        if (null === $this->getCourse()) {
588
            throw new NotFoundHttpException($this->trans('Course not found'));
589
        }
590
        $optionalParams = '';
591
592
        $optionalParams = $request->query->get('cert') ? '&cert='.$request->query->get('cert') : '';
593
594
        if (strpos($link, 'nodeId')) {
595
            $nodeId = (string) $this->getCourse()->getResourceNode()->getId();
596
            $link = str_replace(':nodeId', $nodeId, $link);
597
        }
598
599
        $url = $link.'?'.$this->getCourseUrlQuery().$optionalParams;
600
601
        return $this->redirect($url);
602
    }
603
604
    /**
605
     * Edit configuration with given namespace.
606
     */
607
    #[Route('/{course}/settings/{namespace}', name: 'chamilo_core_course_settings')]
608
    public function updateSettings(
609
        Request $request,
610
        #[MapEntity(expr: 'repository.find(cid)')]
611
        Course $course,
612
        string $namespace,
613
        SettingsCourseManager $manager,
614
        SettingsFormFactory $formFactory
615
    ): Response {
616
        $this->denyAccessUnlessGranted(CourseVoter::VIEW, $course);
617
618
        $schemaAlias = $manager->convertNameSpaceToService($namespace);
619
        $settings = $manager->load($namespace);
620
621
        $form = $formFactory->create($schemaAlias);
622
623
        $form->setData($settings);
624
        $form->handleRequest($request);
625
626
        if ($form->isSubmitted() && $form->isValid()) {
627
            $messageType = 'success';
628
629
            try {
630
                $manager->setCourse($course);
631
                $manager->save($form->getData());
632
                $message = $this->trans('Update');
633
            } catch (ValidatorException $validatorException) {
634
                $message = $this->trans($validatorException->getMessage());
635
                $messageType = 'error';
636
            }
637
            $this->addFlash($messageType, $message);
638
639
            if ($request->headers->has('referer')) {
640
                return $this->redirect($request->headers->get('referer'));
641
            }
642
        }
643
644
        $schemas = $manager->getSchemas();
645
646
        return $this->render(
647
            '@ChamiloCore/Course/settings.html.twig',
648
            [
649
                'course' => $course,
650
                'schemas' => $schemas,
651
                'settings' => $settings,
652
                'form' => $form,
653
            ]
654
        );
655
    }
656
657
    #[Route('/{id}/about', name: 'chamilo_core_course_about')]
658
    public function about(
659
        Course $course,
660
        IllustrationRepository $illustrationRepository,
661
        CCourseDescriptionRepository $courseDescriptionRepository,
662
        EntityManagerInterface $em,
663
        Request $request
664
    ): Response {
665
        $courseId = $course->getId();
666
667
        $user = $this->userHelper->getCurrent();
668
669
        $fieldsRepo = $em->getRepository(ExtraField::class);
670
671
        /** @var TagRepository $tagRepo */
672
        $tagRepo = $em->getRepository(Tag::class);
673
674
        $courseDescriptions = $courseDescriptionRepository->getResourcesByCourse($course)->getQuery()->getResult();
675
676
        $courseValues = new ExtraFieldValue('course');
677
678
        $urlCourse = api_get_path(WEB_PATH).\sprintf('course/%s/about', $courseId);
679
        $courseTeachers = $course->getTeachersSubscriptions();
680
        $teachersData = [];
681
682
        foreach ($courseTeachers as $teacherSubscription) {
683
            $teacher = $teacherSubscription->getUser();
684
            $userData = [
685
                'complete_name' => UserManager::formatUserFullName($teacher),
686
                'image' => $illustrationRepository->getIllustrationUrl($teacher),
687
                'diploma' => $teacher->getDiplomas(),
688
                'openarea' => $teacher->getOpenarea(),
689
            ];
690
691
            $teachersData[] = $userData;
692
        }
693
694
        /** @var ExtraField $tagField */
695
        $tagField = $fieldsRepo->findOneBy([
696
            'itemType' => ExtraField::COURSE_FIELD_TYPE,
697
            'variable' => 'tags',
698
        ]);
699
700
        $courseTags = [];
701
        if (null !== $tagField) {
702
            $courseTags = $tagRepo->getTagsByItem($tagField, $courseId);
703
        }
704
705
        $courseDescription = $courseObjectives = $courseTopics = $courseMethodology = '';
706
        $courseMaterial = $courseResources = $courseAssessment = '';
707
        $courseCustom = [];
708
        foreach ($courseDescriptions as $descriptionTool) {
709
            switch ($descriptionTool->getDescriptionType()) {
710
                case CCourseDescription::TYPE_DESCRIPTION:
711
                    $courseDescription = $descriptionTool->getContent();
712
713
                    break;
714
715
                case CCourseDescription::TYPE_OBJECTIVES:
716
                    $courseObjectives = $descriptionTool;
717
718
                    break;
719
720
                case CCourseDescription::TYPE_TOPICS:
721
                    $courseTopics = $descriptionTool;
722
723
                    break;
724
725
                case CCourseDescription::TYPE_METHODOLOGY:
726
                    $courseMethodology = $descriptionTool;
727
728
                    break;
729
730
                case CCourseDescription::TYPE_COURSE_MATERIAL:
731
                    $courseMaterial = $descriptionTool;
732
733
                    break;
734
735
                case CCourseDescription::TYPE_RESOURCES:
736
                    $courseResources = $descriptionTool;
737
738
                    break;
739
740
                case CCourseDescription::TYPE_ASSESSMENT:
741
                    $courseAssessment = $descriptionTool;
742
743
                    break;
744
745
                case CCourseDescription::TYPE_CUSTOM:
746
                    $courseCustom[] = $descriptionTool;
747
748
                    break;
749
            }
750
        }
751
752
        $topics = [
753
            'objectives' => $courseObjectives,
754
            'topics' => $courseTopics,
755
            'methodology' => $courseMethodology,
756
            'material' => $courseMaterial,
757
            'resources' => $courseResources,
758
            'assessment' => $courseAssessment,
759
            'custom' => array_reverse($courseCustom),
760
        ];
761
762
        $subscriptionUser = false;
763
764
        if ($user) {
765
            $subscriptionUser = CourseManager::is_user_subscribed_in_course($user->getId(), $course->getCode());
766
        }
767
768
        $allowSubscribe = CourseManager::canUserSubscribeToCourse($course->getCode());
769
770
        $image = Container::getIllustrationRepository()->getIllustrationUrl($course, 'course_picture_medium');
771
772
        $params = [
773
            'course' => $course,
774
            'description' => $courseDescription,
775
            'image' => $image,
776
            'syllabus' => $topics,
777
            'tags' => $courseTags,
778
            'teachers' => $teachersData,
779
            'extra_fields' => $courseValues->getAllValuesForAnItem(
780
                $course->getId(),
781
                null,
782
                true
783
            ),
784
            'subscription' => $subscriptionUser,
785
            'url' => '',
786
            'is_premium' => '',
787
            'token' => '',
788
            'base_url' => $request->getSchemeAndHttpHost(),
789
            'allow_subscribe' => $allowSubscribe,
790
        ];
791
792
        $metaInfo = '<meta property="og:url" content="'.$urlCourse.'" />';
793
        $metaInfo .= '<meta property="og:type" content="website" />';
794
        $metaInfo .= '<meta property="og:title" content="'.$course->getTitle().'" />';
795
        $metaInfo .= '<meta property="og:description" content="'.strip_tags($courseDescription).'" />';
796
        $metaInfo .= '<meta property="og:image" content="'.$image.'" />';
797
798
        $htmlHeadXtra[] = $metaInfo;
799
        $htmlHeadXtra[] = api_get_asset('readmore-js/readmore.js');
800
801
        return $this->render('@ChamiloCore/Course/about.html.twig', $params);
802
    }
803
804
    #[Route('/{id}/welcome', name: 'chamilo_core_course_welcome')]
805
    public function welcome(Course $course): Response
806
    {
807
        return $this->render('@ChamiloCore/Course/welcome.html.twig', [
808
            'course' => $course,
809
        ]);
810
    }
811
812
    private function findIntroOfCourse(Course $course): ?CTool
813
    {
814
        $qb = $this->em->createQueryBuilder();
815
816
        $query = $qb->select('ct')
817
            ->from(CTool::class, 'ct')
818
            ->where('ct.course = :c_id')
819
            ->andWhere('ct.title = :title')
820
            ->andWhere(
821
                $qb->expr()->orX(
822
                    $qb->expr()->eq('ct.session', ':session_id'),
823
                    $qb->expr()->isNull('ct.session')
824
                )
825
            )
826
            ->setParameters([
827
                'c_id' => $course->getId(),
828
                'title' => 'course_homepage',
829
                'session_id' => 0,
830
            ])
831
            ->getQuery()
832
        ;
833
834
        $results = $query->getResult();
835
836
        return \count($results) > 0 ? $results[0] : null;
837
    }
838
839
    #[Route('/{id}/getToolIntro', name: 'chamilo_core_course_gettoolintro')]
840
    public function getToolIntro(Request $request, Course $course, EntityManagerInterface $em): Response
841
    {
842
        $sessionId = (int) $request->get('sid');
843
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
1358
        if ($sessionId > 0) {
1359
            $qb
1360
                ->andWhere('IDENTITY(t.session) = :sessionId')
1361
                ->setParameter('sessionId', $sessionId)
1362
            ;
1363
        } else {
1364
            $qb->andWhere('t.session IS NULL');
1365
        }
1366
1367
        /** @var CTool[] $tools */
1368
        $tools = $qb->getQuery()->getResult();
1369
1370
        if (0 === \count($tools)) {
1371
            return [
1372
                'success' => false,
1373
                'message' => 'No tools found for course / session.',
1374
            ];
1375
        }
1376
1377
        // Build an array of IDs to manipulate positions easily
1378
        $ids = array_map(static fn (CTool $tool): int => $tool->getIid() ?? 0, $tools);
1379
1380
        $currentIndex = array_search($toolId, $ids, true);
1381
        if (false === $currentIndex) {
1382
            return [
1383
                'success' => false,
1384
                'message' => 'Tool not found in current course / session.',
1385
            ];
1386
        }
1387
1388
        // Clamp the new index into a valid range
1389
        $newIndex = max(0, min($newIndex, \count($tools) - 1));
1390
1391
        if ($newIndex === $currentIndex) {
1392
            return [
1393
                'success' => true,
1394
                'unchanged' => true,
1395
                'from' => $currentIndex,
1396
                'to' => $newIndex,
1397
                'total' => \count($tools),
1398
            ];
1399
        }
1400
1401
        // Move the ID in the array (remove at old index, insert at new index)
1402
        $idToMove = $ids[$currentIndex];
1403
        array_splice($ids, $currentIndex, 1);
1404
        array_splice($ids, $newIndex, 0, [$idToMove]);
1405
1406
        // Rewrite all positions in DB using a simple DQL UPDATE per tool
1407
        // Positions will be 0-based: 0,1,2,...
1408
        foreach ($ids as $pos => $id) {
1409
            $em->createQueryBuilder()
1410
                ->update(CTool::class, 't')
1411
                ->set('t.position', ':pos')
1412
                ->where('t.iid = :iid')
1413
                ->setParameter('pos', $pos)
1414
                ->setParameter('iid', $id)
1415
                ->getQuery()
1416
                ->execute()
1417
            ;
1418
        }
1419
1420
        return [
1421
            'success' => true,
1422
            'from' => $currentIndex,
1423
            'to' => $newIndex,
1424
            'total' => \count($tools),
1425
            'courseId' => $course->getId(),
1426
            'sessionId' => $sessionId,
1427
        ];
1428
    }
1429
}
1430