Passed
Pull Request — master (#6351)
by
unknown
08:05
created

CourseService::getFallbackAdminUser()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
nc 2
nop 0
dl 0
loc 17
rs 9.9332
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\Service;
8
9
use Agenda;
10
use AnnouncementManager;
11
use Answer;
12
use AppPlugin;
13
use Chamilo\CoreBundle\Entity\Course;
14
use Chamilo\CoreBundle\Entity\CourseRelUser;
15
use Chamilo\CoreBundle\Entity\GradebookCategory;
16
use Chamilo\CoreBundle\Entity\GradebookLink;
17
use Chamilo\CoreBundle\Entity\User;
18
use Chamilo\CoreBundle\Repository\CourseCategoryRepository;
19
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
20
use Chamilo\CoreBundle\Repository\Node\UserRepository;
21
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper;
22
use Chamilo\CoreBundle\Settings\SettingsManager;
23
use Chamilo\CourseBundle\Component\CourseCopy\CourseBuilder;
24
use Chamilo\CourseBundle\Component\CourseCopy\CourseRestorer;
25
use Chamilo\CourseBundle\Entity\CCourseSetting;
26
use Chamilo\CourseBundle\Entity\CForum;
27
use Chamilo\CourseBundle\Entity\CGroupCategory;
28
use DateTime;
29
use DateTimeZone;
30
use Doctrine\ORM\EntityManager;
31
use DocumentManager;
32
use Exception;
33
use Exercise;
34
use InvalidArgumentException;
35
use Link;
36
use LogicException;
37
use MultipleAnswer;
38
use Symfony\Bundle\SecurityBundle\Security;
39
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
40
use Symfony\Component\Finder\Finder;
41
use Symfony\Component\Finder\SplFileInfo;
42
use Symfony\Component\HttpFoundation\RequestStack;
43
use Symfony\Component\Mailer\MailerInterface;
44
use Symfony\Component\Mime\Email;
45
use Symfony\Contracts\Translation\TranslatorInterface;
46
use Throwable;
47
48
class CourseService
49
{
50
    public const MAX_COURSE_LENGTH_CODE = 40;
51
52
    public function __construct(
53
        private readonly EntityManager $entityManager,
54
        private readonly CourseRepository $courseRepository,
55
        private readonly Security $security,
56
        private readonly CourseCategoryRepository $courseCategoryRepository,
57
        private readonly UserRepository $userRepository,
58
        private readonly SettingsManager $settingsManager,
59
        private readonly TranslatorInterface $translator,
60
        private readonly MailerInterface $mailer,
61
        private readonly EventLoggerService $eventLoggerService,
62
        private readonly ParameterBagInterface $parameterBag,
63
        private readonly RequestStack $requestStack,
64
        private readonly AccessUrlHelper $accessUrlHelper
65
    ) {}
66
67
    public function createCourse(array $params): ?Course
68
    {
69
        if (empty($params['title'])) {
70
            throw new InvalidArgumentException('The course title cannot be empty.');
71
        }
72
73
        if (empty($params['wanted_code'])) {
74
            $params['wanted_code'] = $this->generateCourseCode($params['title']);
75
        }
76
77
        if ($this->courseRepository->courseCodeExists($params['wanted_code'])) {
78
            throw new Exception('The course code already exists: '.$params['wanted_code']);
79
        }
80
81
        $keys = $this->defineCourseKeys($params['wanted_code']);
82
83
        $params = array_merge($params, $keys);
84
        $course = $this->registerCourse($params);
85
        if ($course) {
86
            $this->handlePostCourseCreation($course, $params);
87
        }
88
89
        return $course;
90
    }
91
92
    public function registerCourse(array $rawParams): ?Course
93
    {
94
        try {
95
            /** @var User|null $currentUser */
96
            $currentUser = $this->security->getUser();
97
98
            // Fallback admin user if running from CLI
99
            if (!$currentUser instanceof User) {
100
                $currentUser = $this->getFallbackAdminUser();
101
            }
102
103
            $params = $this->prepareAndValidateCourseData($rawParams);
104
            $accessUrl = $this->accessUrlHelper->getCurrent();
105
            $course = new Course();
106
            $course
107
                ->setTitle($params['title'])
108
                ->setCode($params['code'])
109
                ->setVisualCode($params['visualCode'])
110
                ->setCourseLanguage($params['courseLanguage'])
111
                ->setDescription($this->translator->trans('Course Description'))
112
                ->setVisibility((int) $params['visibility'])
113
                ->setShowScore(1)
114
                ->setDiskQuota((int) $params['diskQuota'])
115
                ->setExpirationDate($params['expirationDate'])
116
                ->setDepartmentName($params['departmentName'] ?? '')
117
                ->setDepartmentUrl($params['departmentUrl'])
118
                ->setSubscribe($params['subscribe'])
119
                ->setSticky($params['sticky'] ?? false)
120
                ->setVideoUrl($params['videoUrl'] ?? '')
121
                ->setUnsubscribe($params['unsubscribe'])
122
                ->setCreator($currentUser)
123
            ;
124
            $course->addAccessUrl($accessUrl);
125
126
            if (!empty($params['categories'])) {
127
                foreach ($params['categories'] as $categoryId) {
128
                    $category = $this->courseCategoryRepository->find($categoryId);
129
                    if ($category) {
130
                        $course->addCategory($category);
131
                    }
132
                }
133
            }
134
135
            $addTeacher = $params['add_user_as_teacher'] ?? true;
136
            $user = $currentUser ?? $this->getFallbackAdminUser();
137
            if (!empty($params['user_id'])) {
138
                $user = $this->userRepository->find((int) $params['user_id']);
139
            }
140
            if ($addTeacher) {
141
                $courseRelTutor = (new CourseRelUser())
142
                    ->setCourse($course)
143
                    ->setUser($user)
144
                    ->setStatus(1)
145
                    ->setTutor(true)
146
                    ->setRelationType(0)
147
                    ->setUserCourseCat(0)
148
                ;
149
                $course->addSubscription($courseRelTutor);
150
            }
151
152
            if (!empty($params['teachers'])) {
153
                foreach ($params['teachers'] as $teacherId) {
154
                    $teacher = $this->userRepository->find($teacherId);
155
                    if ($teacher) {
156
                        $courseRelTeacher = (new CourseRelUser())
157
                            ->setCourse($course)
158
                            ->setUser($teacher)
159
                            ->setStatus(1)
160
                            ->setTutor(false)
161
                            ->setRelationType(0)
162
                            ->setUserCourseCat(0)
163
                        ;
164
                        $course->addSubscription($courseRelTeacher);
165
                    }
166
                }
167
            }
168
169
            $this->courseRepository->create($course);
170
171
            if (!empty($rawParams['exemplary_content'])) {
172
                $this->fillCourse($course, $params);
173
            }
174
175
            if (isset($rawParams['course_template'])) {
176
                $this->useTemplateAsBasisIfRequired(
177
                    $course->getCode(),
178
                    (int) $rawParams['course_template']
179
                );
180
            }
181
182
            return $course;
183
        } catch (Exception $e) {
184
            throw $e;
185
        }
186
    }
187
188
    public function sendEmailToAdmin(Course $course): void
189
    {
190
        $siteName = $this->getDefaultSetting('platform.site_name');
191
        $recipientEmail = $this->getDefaultSetting('admin.administrator_email');
192
        $recipientName = $this->getDefaultSetting('admin.administrator_name').' '.$this->getDefaultSetting('admin.administrator_surname');
193
        $institutionName = $this->getDefaultSetting('platform.institution');
194
        $courseName = $course->getTitle();
195
196
        $subject = $this->translator->trans('New Course Created in')."$siteName - $institutionName";
197
198
        $greeting = $this->translator->trans('email.greeting');
199
        $intro = $this->translator->trans('email.course_created_intro');
200
        $courseNameLabel = $this->translator->trans('email.course_name');
201
202
        $message = "$greeting $recipientName,\n\n";
203
        $message .= "$intro $siteName - $institutionName.\n";
204
        $message .= "$courseNameLabel $courseName\n";
205
        $message .= $this->translator->trans('Course name: ').$course->getTitle()."\n";
206
207
        foreach ($course->getCategories() as $category) {
208
            $message .= $this->translator->trans('Category: ').$category->getCode()."\n";
209
        }
210
211
        $message .= $this->translator->trans('Coach: ').$course->getTutorName()."\n";
212
        $message .= $this->translator->trans('Language: ').$course->getCourseLanguage();
213
214
        $email = (new Email())
215
            ->from($recipientEmail)
216
            ->to($recipientEmail)
217
            ->subject($subject)
218
            ->text($message)
219
        ;
220
221
        $this->mailer->send($email);
222
    }
223
224
    public function defineCourseKeys(
225
        string $wantedCode,
226
        string $prefixForAll = '',
227
        string $prefixForPath = '',
228
        bool $addUniquePrefix = false,
229
        bool $useCodeIndependentKeys = true
230
    ): array {
231
        $wantedCode = $this->generateCourseCode($wantedCode);
232
        $keysCourseCode = $useCodeIndependentKeys ? $wantedCode : '';
233
234
        $uniquePrefix = $addUniquePrefix ? substr(md5(uniqid((string) rand(), true)), 0, 10) : '';
235
236
        $keys = [];
237
        $finalSuffix = ['CourseId' => '', 'CourseDir' => ''];
238
        $limitNumTry = 100;
239
        $tryCount = 0;
240
241
        $keysAreUnique = false;
242
243
        while (!$keysAreUnique && $tryCount < $limitNumTry) {
244
            $keysCourseId = $prefixForAll.$uniquePrefix.$keysCourseCode.$finalSuffix['CourseId'];
245
            $keysCourseRepository = $prefixForPath.$uniquePrefix.$wantedCode.$finalSuffix['CourseDir'];
246
247
            if ($this->courseRepository->courseCodeExists($keysCourseId)) {
248
                $finalSuffix['CourseId'] = substr(md5(uniqid((string) rand(), true)), 0, 4);
249
                $tryCount++;
250
            } else {
251
                $keysAreUnique = true;
252
            }
253
        }
254
255
        if ($keysAreUnique) {
256
            $keys = [
257
                'code' => $keysCourseCode,
258
                'visual_code' => $keysCourseId,
259
                'directory' => $keysCourseRepository,
260
            ];
261
        }
262
263
        return $keys;
264
    }
265
266
    public function fillCourse(Course $course, array $params): void
267
    {
268
        $entityManager = $this->entityManager;
269
270
        $this->insertCourseSettings($course);
271
272
        $this->createGroupCategory($course);
273
274
        $gradebook = $this->createRootGradebook($course);
275
276
        if ('true' === $this->settingsManager->getSetting('course.example_material_course_creation')) {
277
            $this->insertExampleContent($course, $gradebook);
278
        }
279
280
        $this->installCoursePlugins($course->getId());
281
282
        $entityManager->flush();
283
    }
284
285
    private function insertCourseSettings(Course $course): void
286
    {
287
        $defaultEmailExerciseAlert = 0;
288
        if ('true' === $this->settingsManager->getSetting('exercise.email_alert_manager_on_new_quiz')) {
289
            $defaultEmailExerciseAlert = 1;
290
        }
291
292
        $settings = [
293
            'email_alert_manager_on_new_doc' => ['title' => '', 'default' => 0, 'category' => 'work'],
294
            'email_alert_on_new_doc_dropbox' => ['default' => 0, 'category' => 'dropbox'],
295
            'allow_user_edit_agenda' => ['default' => 0, 'category' => 'agenda'],
296
            'allow_user_edit_announcement' => ['default' => 0, 'category' => 'announcement'],
297
            'email_alert_manager_on_new_quiz' => ['default' => $defaultEmailExerciseAlert, 'category' => 'quiz'],
298
            'allow_user_image_forum' => ['default' => 1, 'category' => 'forum'],
299
            'course_theme' => ['default' => '', 'category' => 'theme'],
300
            'allow_learning_path_theme' => ['default' => 1, 'category' => 'theme'],
301
            'allow_open_chat_window' => ['default' => 1, 'category' => 'chat'],
302
            'email_alert_to_teacher_on_new_user_in_course' => ['default' => 0, 'category' => 'registration'],
303
            'allow_user_view_user_list' => ['default' => 1, 'category' => 'user'],
304
            'display_info_advance_inside_homecourse' => ['default' => 1, 'category' => 'thematic_advance'],
305
            'email_alert_students_on_new_homework' => ['default' => 0, 'category' => 'work'],
306
            'enable_lp_auto_launch' => ['default' => 0, 'category' => 'learning_path'],
307
            'enable_exercise_auto_launch' => ['default' => 0, 'category' => 'exercise'],
308
            'enable_document_auto_launch' => ['default' => 0, 'category' => 'document'],
309
            'pdf_export_watermark_text' => ['default' => '', 'category' => 'learning_path'],
310
            'allow_public_certificates' => [
311
                'default' => 'true' === $this->settingsManager->getSetting('course.allow_public_certificates') ? 1 : '',
312
                'category' => 'certificates',
313
            ],
314
            'documents_default_visibility' => ['default' => 'visible', 'category' => 'document'],
315
            'show_course_in_user_language' => ['default' => 2, 'category' => null],
316
            'email_to_teachers_on_new_work_feedback' => ['default' => 1, 'category' => null],
317
        ];
318
319
        foreach ($settings as $variable => $setting) {
320
            $courseSetting = new CCourseSetting();
321
            $courseSetting->setCId($course->getId());
322
            $courseSetting->setVariable($variable);
323
            $courseSetting->setTitle($setting['title'] ?? '');
324
            $courseSetting->setValue((string) $setting['default']);
325
            $courseSetting->setCategory($setting['category'] ?? '');
326
327
            $this->entityManager->persist($courseSetting);
328
        }
329
330
        $this->entityManager->flush();
331
    }
332
333
    private function createGroupCategory(Course $course): void
334
    {
335
        $groupCategory = new CGroupCategory();
336
        $groupCategory
337
            ->setTitle($this->translator->trans('Default groups'))
338
            ->setParent($course)
339
            ->addCourseLink($course)
340
        ;
341
342
        $this->entityManager->persist($groupCategory);
343
        $this->entityManager->flush();
344
    }
345
346
    private function createRootGradebook(Course $course): GradebookCategory
347
    {
348
        /** @var User $currentUser */
349
        $currentUser = $this->security->getUser();
350
        if (!$currentUser instanceof User) {
351
            $currentUser = $this->getFallbackAdminUser();
352
        }
353
354
        if (!$currentUser) {
355
            throw new LogicException('There is no user currently authenticated..');
356
        }
357
358
        $gradebookCategory = new GradebookCategory();
359
        $gradebookCategory
360
            ->setTitle($course->getCode())
361
            ->setLocked(0)
362
            ->setGenerateCertificates(false)
363
            ->setDescription('')
364
            ->setCourse($course)
365
            ->setWeight(100)
366
            ->setVisible(false)
367
            ->setCertifMinScore(75)
368
            ->setUser($currentUser)
369
        ;
370
371
        $this->entityManager->persist($gradebookCategory);
372
        $this->entityManager->flush();
373
374
        return $gradebookCategory;
375
    }
376
377
    private function insertExampleContent(Course $course, GradebookCategory $gradebook): void
378
    {
379
        /** @var User $currentUser */
380
        $currentUser = $this->security->getUser();
381
        if (!$currentUser instanceof User) {
382
            $currentUser = $this->getFallbackAdminUser();
383
        }
384
385
        $files = [
386
            ['path' => '/audio', 'title' => $this->translator->trans('Audio'), 'filetype' => 'folder', 'size' => 0],
387
            ['path' => '/images', 'title' => $this->translator->trans('Images'), 'filetype' => 'folder', 'size' => 0],
388
            ['path' => '/images/gallery', 'title' => $this->translator->trans('Gallery'), 'filetype' => 'folder', 'size' => 0],
389
            ['path' => '/video', 'title' => $this->translator->trans('Video'), 'filetype' => 'folder', 'size' => 0],
390
        ];
391
        $paths = [];
392
        $courseInfo = ['real_id' => $course->getId(), 'code' => $course->getCode()];
393
        foreach ($files as $file) {
394
            $doc = DocumentManager::addDocument(
395
                $courseInfo,
396
                $file['path'],
397
                $file['filetype'],
398
                $file['size'],
399
                $file['title'],
400
                null,
401
                0,
402
                null,
403
                0,
404
                0,
405
                0,
406
                false
407
            );
408
            $paths[$file['path']] = $doc->getIid();
409
        }
410
411
        $projectDir = $this->parameterBag->get('kernel.project_dir');
412
        $defaultPath = $projectDir.'/public/img/document';
413
414
        $request = $this->requestStack->getCurrentRequest();
415
        $baseUrl = $request->getSchemeAndHttpHost().$request->getBasePath();
416
417
        $finder = new Finder();
418
        $finder->in($defaultPath);
419
420
        /** @var SplFileInfo $file */
421
        foreach ($finder as $file) {
422
            $parentName = \dirname(str_replace($defaultPath, '', $file->getRealPath()));
423
            if ('/' === $parentName || '/certificates' === $parentName) {
424
                continue;
425
            }
426
427
            $title = $file->getFilename();
428
            $parentId = $paths[$parentName];
429
430
            if ($file->isDir()) {
431
                $realPath = str_replace($defaultPath, '', $file->getRealPath());
432
                $document = DocumentManager::addDocument(
433
                    $courseInfo,
434
                    $realPath,
435
                    'folder',
436
                    null,
437
                    $title,
438
                    '',
439
                    null,
440
                    null,
441
                    null,
442
                    null,
443
                    null,
444
                    false,
445
                    null,
446
                    $parentId,
447
                    $file->getRealPath()
448
                );
449
                $paths[$realPath] = $document->getIid();
450
            } else {
451
                $realPath = str_replace($defaultPath, '', $file->getRealPath());
452
                $document = DocumentManager::addDocument(
453
                    $courseInfo,
454
                    $realPath,
455
                    'file',
456
                    $file->getSize(),
457
                    $title,
458
                    '',
459
                    null,
460
                    null,
461
                    null,
462
                    null,
463
                    null,
464
                    false,
465
                    null,
466
                    $parentId,
467
                    $file->getRealPath()
468
                );
469
            }
470
        }
471
472
        $now = new DateTime('now', new DateTimeZone('UTC'));
473
        $formattedNow = $now->format('Y-m-d H:i:s');
474
475
        $agenda = new Agenda('course');
476
        $agenda->set_course($courseInfo);
477
        $agenda->addEvent(
478
            $formattedNow,
479
            $formattedNow,
480
            0,
481
            $this->translator->trans('Course creation'),
482
            $this->translator->trans('This course was created at this time'),
483
            ['everyone' => 'everyone']
484
        );
485
486
        /*  Links tool */
487
        $link = new Link();
488
        $link->setCourse($courseInfo);
489
        $links = [
490
            [
491
                'c_id' => $course->getId(),
492
                'url' => 'http://www.google.com',
493
                'title' => 'Quick and powerful search engine',
494
                'description' => $this->translator->trans('Quick and powerful search engine'),
495
                'category_id' => 0,
496
                'on_homepage' => 0,
497
                'target' => '_self',
498
                'session_id' => 0,
499
            ],
500
            [
501
                'c_id' => $course->getId(),
502
                'url' => 'http://www.wikipedia.org',
503
                'title' => 'Free online encyclopedia',
504
                'description' => $this->translator->trans('Free online encyclopedia'),
505
                'category_id' => 0,
506
                'on_homepage' => 0,
507
                'target' => '_self',
508
                'session_id' => 0,
509
            ],
510
        ];
511
512
        foreach ($links as $params) {
513
            $link->save($params, false, false);
514
        }
515
516
        /* Announcement tool */
517
        AnnouncementManager::add_announcement(
518
            $courseInfo,
519
            0,
520
            $this->translator->trans('This is an announcement example'),
521
            $this->translator->trans('This is an announcement example. Only trainers are allowed to publish announcements.'),
522
            ['everyone' => 'everyone'],
523
            null,
524
            null,
525
            $formattedNow
526
        );
527
528
        /*  Exercise tool */
529
        $exercise = new Exercise($course->getId());
530
        $exercise->exercise = $this->translator->trans('Sample test');
531
        $html = '<table width="100%" border="0" cellpadding="0" cellspacing="0">
532
                        <tr>
533
                        <td width="220" valign="top" align="left">
534
                            <img src="'.$baseUrl.'/public/img/document/images/mr_chamilo/doubts.png">
535
                        </td>
536
                        <td valign="top" align="left">'.$this->translator->trans('Irony').'</td></tr>
537
                    </table>';
538
        $exercise->type = 1;
539
        $exercise->setRandom(0);
540
        $exercise->active = 1;
541
        $exercise->results_disabled = 0;
542
        $exercise->description = $html;
543
        $exercise->save();
544
545
        $question = new MultipleAnswer();
546
        $question->course = $courseInfo;
547
        $question->question = $this->translator->trans('Socratic irony is...');
548
        $question->description = $this->translator->trans('(more than one answer can be true)');
549
        $question->weighting = 10;
550
        $question->position = 1;
551
        $question->course = $courseInfo;
552
        $question->save($exercise);
553
        $questionId = $question->id;
554
555
        $answer = new Answer($questionId, $courseInfo['real_id']);
556
        $answer->createAnswer($this->translator->trans('Ridiculise one\'s interlocutor in order to have him concede he is wrong.'), 0, $this->translator->trans('No. Socratic irony is not a matter of psychology, it concerns argumentation.'), -5, 1);
557
        $answer->createAnswer($this->translator->trans('Admit one\'s own errors to invite one\'s interlocutor to do the same.'), 0, $this->translator->trans('No. Socratic irony is not a seduction strategy or a method based on the example.'), -5, 2);
558
        $answer->createAnswer($this->translator->trans('Compell one\'s interlocutor, by a series of questions and sub-questions, to admit he doesn\'t know what he claims to know.'), 1, $this->translator->trans('Indeed'), 5, 3);
559
        $answer->createAnswer($this->translator->trans('Use the Principle of Non Contradiction to force one\'s interlocutor into a dead end.'), 1, $this->translator->trans('This answer is not false. It is true that the revelation of the interlocutor\'s ignorance means showing the contradictory conclusions where lead his premisses.'), 5, 4);
560
        $answer->save();
561
562
        // Forums.
563
        $params = [
564
            'forum_category_title' => $this->translator->trans('Example Forum Category'),
565
            'forum_category_comment' => '',
566
        ];
567
568
        $forumCategoryId = saveForumCategory($params, $courseInfo, false);
569
570
        $params = [
571
            'forum_category' => $forumCategoryId,
572
            'forum_title' => $this->translator->trans('Example Forum'),
573
            'forum_comment' => '',
574
            'default_view_type_group' => ['default_view_type' => 'flat'],
575
        ];
576
577
        $forumId = store_forum($params, $courseInfo, true);
578
        $repo = $this->entityManager->getRepository(CForum::class);
579
        $forumEntity = $repo->find($forumId);
580
581
        $params = [
582
            'post_title' => $this->translator->trans('Example Thread'),
583
            'forum_id' => $forumId,
584
            'post_text' => $this->translator->trans('Example ThreadContent'),
585
            'calification_notebook_title' => '',
586
            'numeric_calification' => '',
587
            'weight_calification' => '',
588
            'forum_category' => $forumCategoryId,
589
            'thread_peer_qualify' => 0,
590
        ];
591
592
        saveThread($forumEntity, $params, $courseInfo, false);
593
594
        $this->createExampleGradebookContent($course, $gradebook, $exercise->id);
595
    }
596
597
    private function createExampleGradebookContent(Course $course, GradebookCategory $parentCategory, int $refId): void
598
    {
599
        $manager = $this->entityManager;
600
601
        /* Gradebook tool */
602
        $courseCode = $course->getCode();
603
604
        $childGradebookCategory = new GradebookCategory();
605
        $childGradebookCategory->setTitle($courseCode);
606
        $childGradebookCategory->setLocked(0);
607
        $childGradebookCategory->setGenerateCertificates(false);
608
        $childGradebookCategory->setDescription('');
609
        $childGradebookCategory->setCourse($course);
610
        $childGradebookCategory->setWeight(100);
611
        $childGradebookCategory->setVisible(true);
612
        $childGradebookCategory->setCertifMinScore(75);
613
        $childGradebookCategory->setParent($parentCategory);
614
        $childGradebookCategory->setUser(api_get_user_entity());
615
616
        $manager->persist($childGradebookCategory);
617
        $manager->flush();
618
619
        $gradebookLink = new GradebookLink();
620
621
        $gradebookLink->setType(1);
622
        $gradebookLink->setRefId($refId);
623
        $gradebookLink->setUser(api_get_user_entity());
624
        $gradebookLink->setCourse($course);
625
        $gradebookLink->setCategory($childGradebookCategory);
626
        $gradebookLink->setCreatedAt(new DateTime());
627
        $gradebookLink->setWeight(100);
628
        $gradebookLink->setVisible(1);
629
        $gradebookLink->setLocked(0);
630
631
        $manager->persist($gradebookLink);
632
        $manager->flush();
633
    }
634
635
    private function installCoursePlugins(int $courseId): void
636
    {
637
        $app_plugin = new AppPlugin();
638
        $app_plugin->install_course_plugins($courseId);
639
    }
640
641
    public function useTemplateAsBasisIfRequired($courseCode, $courseTemplate): void
642
    {
643
        $templateSetting = $this->settingsManager->getSetting('course.course_creation_use_template');
644
        $teacherCanSelectCourseTemplate = 'true' === $this->settingsManager->getSetting('course.teacher_can_select_course_template');
645
        $courseTemplate = isset($courseTemplate) ? (int) $courseTemplate : 0;
646
647
        $useTemplate = false;
648
        if ($teacherCanSelectCourseTemplate && $courseTemplate > 0) {
649
            $useTemplate = true;
650
            $originCourse = $this->courseRepository->findCourseAsArray((int) $courseTemplate);
651
        } elseif (!empty($templateSetting)) {
652
            $useTemplate = true;
653
            $originCourse = $this->courseRepository->findCourseAsArray((int) $templateSetting);
654
        }
655
656
        if ($useTemplate && !empty($originCourse)) {
657
            try {
658
                $originCourse['official_code'] = $originCourse['code'];
659
                $cb = new CourseBuilder(null, $originCourse);
660
                $course = $cb->build(null, $originCourse['code']);
661
                $cr = new CourseRestorer($course);
662
                $cr->set_file_option();
663
                $cr->restore($courseCode);
664
            } catch (Exception $e) {
665
                error_log('Error during course template application: '.$e->getMessage());
666
            } catch (Throwable $t) {
667
                error_log('Unexpected error during course template application: '.$t->getMessage());
668
            }
669
        }
670
    }
671
672
    private function prepareAndValidateCourseData(array $params): array
673
    {
674
        $title = str_replace('&amp;', '&', $params['title'] ?? '');
675
        $code = $params['code'] ?? '';
676
        $visualCode = $params['visual_code'] ?? '';
677
        $directory = $params['directory'] ?? '';
678
        $tutorName = $params['tutor_name'] ?? null;
679
        $courseLanguage = !empty($params['course_language']) ? $params['course_language'] : $this->getDefaultSetting('language.platform_language');
680
        $departmentName = $params['department_name'] ?? null;
681
        $departmentUrl = $this->fixDepartmentUrl($params['department_url'] ?? '');
682
        $diskQuota = $params['disk_quota'] ?? $this->getDefaultSetting('document.default_document_quotum');
683
        $visibility = $params['visibility'] ?? $this->getDefaultSetting('course.courses_default_creation_visibility', Course::OPEN_PLATFORM);
684
        $subscribe = $params['subscribe'] ?? (Course::OPEN_PLATFORM == $visibility);
685
        $unsubscribe = $params['unsubscribe'] ?? false;
686
        $expirationDate = $params['expiration_date'] ?? $this->getFutureExpirationDate();
687
        $teachers = $params['teachers'] ?? [];
688
        $categories = $params['course_categories'] ?? [];
689
        $notifyAdmins = $this->getDefaultSetting('course.send_email_to_admin_when_create_course');
690
691
        $errors = [];
692
        if (empty($code)) {
693
            $errors[] = 'courseSysCode is missing';
694
        }
695
        if (empty($visualCode)) {
696
            $errors[] = 'courseScreenCode is missing';
697
        }
698
        if (empty($directory)) {
699
            $errors[] = 'courseRepository is missing';
700
        }
701
        if (empty($title)) {
702
            $errors[] = 'title is missing';
703
        }
704
        if ($visibility < 0 || $visibility > 4) {
705
            $errors[] = 'visibility is invalid';
706
        }
707
708
        if (!empty($errors)) {
709
            throw new Exception(implode(', ', $errors));
710
        }
711
712
        return [
713
            'title' => $title,
714
            'code' => $code,
715
            'visualCode' => $visualCode,
716
            'directory' => $directory,
717
            'tutorName' => $tutorName,
718
            'courseLanguage' => $courseLanguage,
719
            'departmentName' => $departmentName,
720
            'departmentUrl' => $departmentUrl,
721
            'diskQuota' => $diskQuota,
722
            'visibility' => $visibility,
723
            'subscribe' => $subscribe,
724
            'unsubscribe' => $unsubscribe,
725
            'expirationDate' => new DateTime($expirationDate),
726
            'teachers' => $teachers,
727
            'categories' => $categories,
728
            'notifyAdmins' => $notifyAdmins,
729
        ];
730
    }
731
732
    private function getDefaultSetting(string $name, $default = null)
733
    {
734
        $settingValue = $this->settingsManager->getSetting($name);
735
736
        return null !== $settingValue ? $settingValue : $default;
737
    }
738
739
    private function fixDepartmentUrl(string $url): string
740
    {
741
        if (!empty($url) && !str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
742
            return 'http://'.$url;
743
        }
744
745
        return 'http://' === $url ? '' : $url;
746
    }
747
748
    private function getFutureExpirationDate(): string
749
    {
750
        return (new DateTime())->modify('+1 year')->format('Y-m-d H:i:s');
751
    }
752
753
    private function generateCourseCode(string $title): string
754
    {
755
        $cleanTitle = preg_replace('/[^A-Z0-9]/', '', strtoupper($this->replaceDangerousChar($title)));
756
757
        return substr($cleanTitle, 0, self::MAX_COURSE_LENGTH_CODE);
758
    }
759
760
    private function replaceDangerousChar(string $text): string
761
    {
762
        $encoding = mb_detect_encoding($text, mb_detect_order(), true);
763
        if ('UTF-8' !== $encoding) {
764
            $text = mb_convert_encoding($text, 'UTF-8', $encoding);
765
        }
766
767
        $text = str_replace(' ', '-', $text);
768
        $text = preg_replace('/[^-\w]+/', '', $text);
769
770
        return preg_replace('/\.+$/', '', $text);
771
    }
772
773
    private function handlePostCourseCreation(Course $course, array $params): void
774
    {
775
        if ($params['notifyAdmins'] ?? false) {
776
            $this->sendEmailToAdmin($course);
777
        }
778
779
        $currentUser = $this->security->getUser();
780
        if (!$currentUser instanceof User) {
781
            $currentUser = $this->getFallbackAdminUser();
782
        }
783
784
        $this->eventLoggerService->addEvent(
785
            'course_created',
786
            'course_id',
787
            $course->getId(),
788
            null,
789
            $currentUser->getId(),
790
            $course->getId()
791
        );
792
    }
793
794
    private function getFallbackAdminUser(): User
795
    {
796
        $qb = $this->entityManager->createQueryBuilder();
797
798
        $qb->select('u')
799
            ->from(User::class, 'u')
800
            ->where('u.roles LIKE :role')
801
            ->setParameter('role', '%ROLE_ADMIN%')
802
            ->setMaxResults(1);
803
804
        $user = $qb->getQuery()->getOneOrNullResult();
805
806
        if (!$user instanceof User) {
807
            throw new \RuntimeException('No admin user found for fallback.');
808
        }
809
810
        return $user;
811
    }
812
}
813