Passed
Pull Request — master (#6673)
by
unknown
08:47
created

CourseHelper::insertCourseSettings()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 46
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 36
nc 8
nop 1
dl 0
loc 46
rs 9.344
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\Helpers;
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\Settings\SettingsManager;
22
use Chamilo\CourseBundle\Component\CourseCopy\CourseBuilder;
23
use Chamilo\CourseBundle\Component\CourseCopy\CourseRestorer;
24
use Chamilo\CourseBundle\Entity\CCourseSetting;
25
use Chamilo\CourseBundle\Entity\CForum;
26
use Chamilo\CourseBundle\Entity\CGroupCategory;
27
use DateTime;
28
use DateTimeZone;
29
use Doctrine\ORM\EntityManager;
30
use DocumentManager;
31
use Exception;
32
use Exercise;
33
use InvalidArgumentException;
34
use Link;
35
use LogicException;
36
use MultipleAnswer;
37
use RuntimeException;
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 CourseHelper
49
{
50
    public const MAX_COURSE_LENGTH_CODE = 40;
51
    private bool $debug = false;
52
53
    public function __construct(
54
        private readonly EntityManager $entityManager,
55
        private readonly CourseRepository $courseRepository,
56
        private readonly Security $security,
57
        private readonly CourseCategoryRepository $courseCategoryRepository,
58
        private readonly UserRepository $userRepository,
59
        private readonly SettingsManager $settingsManager,
60
        private readonly TranslatorInterface $translator,
61
        private readonly MailerInterface $mailer,
62
        private readonly EventLoggerHelper $eventLoggerHelper,
63
        private readonly ParameterBagInterface $parameterBag,
64
        private readonly RequestStack $requestStack,
65
        private readonly AccessUrlHelper $accessUrlHelper
66
    ) {}
67
68
    public function createCourse(array $params): ?Course
69
    {
70
        $this->debugLog('createCourse:start', [
71
            'title' => $params['title'] ?? null,
72
            'wanted_code' => $params['wanted_code'] ?? null,
73
            'exemplary_content' => !empty($params['exemplary_content']),
74
        ]);
75
76
        if (empty($params['title'])) {
77
            throw new InvalidArgumentException('The course title cannot be empty.');
78
        }
79
80
        if (empty($params['wanted_code'])) {
81
            $params['wanted_code'] = $this->generateCourseCode($params['title']);
82
            $this->debugLog('createCourse:generatedWantedCode', ['wanted_code' => $params['wanted_code']]);
83
        }
84
85
        if ($this->courseRepository->courseCodeExists($params['wanted_code'])) {
86
            $this->debugLog('createCourse:duplicateCode', ['wanted_code' => $params['wanted_code']]);
87
            throw new Exception('The course code already exists: '.$params['wanted_code']);
88
        }
89
90
        $keys = $this->defineCourseKeys($params['wanted_code']);
91
        $this->debugLog('createCourse:keys', $keys);
92
93
        $params = array_merge($params, $keys);
94
        $course = $this->registerCourse($params);
95
        if ($course) {
96
            $this->debugLog('createCourse:registered', ['courseId' => $course->getId()]);
97
            $this->handlePostCourseCreation($course, $params);
98
            $this->debugLog('createCourse:done', ['courseId' => $course->getId()]);
99
        }
100
101
        return $course;
102
    }
103
104
    public function registerCourse(array $rawParams): ?Course
105
    {
106
        $this->debugLog('registerCourse:start', [
107
            'exemplary_content_raw' => !empty($rawParams['exemplary_content']),
108
            'has_template' => isset($rawParams['course_template']) && $rawParams['course_template'] !== '',
109
        ]);
110
111
        try {
112
            /** @var User|null $currentUser */
113
            $currentUser = $this->security->getUser();
114
115
            // Fallback admin user if running from CLI
116
            if (!$currentUser instanceof User) {
117
                $currentUser = $this->getFallbackAdminUser();
118
                $this->debugLog('registerCourse:fallbackAdmin', ['adminId' => $currentUser->getId()]);
119
            }
120
121
            $params = $this->prepareAndValidateCourseData($rawParams);
122
            $params['_exemplary_content_request'] = !empty($rawParams['exemplary_content']);
123
124
            $accessUrl = $this->accessUrlHelper->getCurrent();
125
            $course = new Course();
126
            $course
127
                ->setTitle($params['title'])
128
                ->setCode($params['code'])
129
                ->setVisualCode($params['visualCode'])
130
                ->setCourseLanguage($params['courseLanguage'])
131
                ->setDescription($this->translator->trans('Course Description'))
132
                ->setVisibility((int) $params['visibility'])
133
                ->setShowScore(1)
134
                ->setDiskQuota((int) $params['diskQuota'])
135
                ->setExpirationDate($params['expirationDate'])
136
                ->setDepartmentName($params['departmentName'] ?? '')
137
                ->setDepartmentUrl($params['departmentUrl'])
138
                ->setSubscribe($params['subscribe'])
139
                ->setSticky($params['sticky'] ?? false)
140
                ->setVideoUrl($params['videoUrl'] ?? '')
141
                ->setUnsubscribe($params['unsubscribe'])
142
                ->setCreator($currentUser)
143
            ;
144
            $course->addAccessUrl($accessUrl);
145
146
            if (!empty($params['categories'])) {
147
                $this->debugLog('registerCourse:categoriesAttach', ['count' => count($params['categories'])]);
148
                foreach ($params['categories'] as $categoryId) {
149
                    $category = $this->courseCategoryRepository->find($categoryId);
150
                    if ($category) {
151
                        $course->addCategory($category);
152
                    }
153
                }
154
            }
155
156
            $addTeacher = $params['add_user_as_teacher'] ?? true;
157
            $user = $currentUser ?? $this->getFallbackAdminUser();
158
            if (!empty($params['user_id'])) {
159
                $user = $this->userRepository->find((int) $params['user_id']);
160
                $this->debugLog('registerCourse:explicitUser', ['userId' => $user?->getId()]);
161
            }
162
            if ($addTeacher) {
163
                $courseRelTutor = (new CourseRelUser())
164
                    ->setCourse($course)
165
                    ->setUser($user)
166
                    ->setStatus(1)
167
                    ->setTutor(true)
168
                    ->setRelationType(0)
169
                    ->setUserCourseCat(0)
170
                ;
171
                $course->addSubscription($courseRelTutor);
172
            }
173
174
            if (!empty($params['teachers'])) {
175
                $this->debugLog('registerCourse:additionalTeachers', ['count' => count($params['teachers'])]);
176
                foreach ($params['teachers'] as $teacherId) {
177
                    $teacher = $this->userRepository->find($teacherId);
178
                    if ($teacher) {
179
                        $courseRelTeacher = (new CourseRelUser())
180
                            ->setCourse($course)
181
                            ->setUser($teacher)
182
                            ->setStatus(1)
183
                            ->setTutor(false)
184
                            ->setRelationType(0)
185
                            ->setUserCourseCat(0)
186
                        ;
187
                        $course->addSubscription($courseRelTeacher);
188
                    }
189
                }
190
            }
191
192
            $this->courseRepository->create($course);
193
            $this->debugLog('registerCourse:persisted', ['courseId' => $course->getId()]);
194
195
            if (!empty($rawParams['exemplary_content'])) {
196
                $this->debugLog('registerCourse:willFillCourse', [
197
                    'request_demo' => true,
198
                ]);
199
                $this->fillCourse($course, $params);
200
            } else {
201
                $this->debugLog('registerCourse:skipFillCourse', [
202
                    'request_demo' => false,
203
                ]);
204
            }
205
206
            if (isset($rawParams['course_template'])) {
207
                $this->debugLog('registerCourse:maybeTemplate', ['template' => (int) $rawParams['course_template']]);
208
                $this->useTemplateAsBasisIfRequired(
209
                    $course->getCode(),
210
                    (int) $rawParams['course_template']
211
                );
212
            }
213
214
            $this->debugLog('registerCourse:done', ['courseId' => $course->getId()]);
215
            return $course;
216
        } catch (Exception $e) {
217
            $this->debugLog('registerCourse:exception', ['msg' => $e->getMessage()]);
218
            throw $e;
219
        }
220
    }
221
222
    public function sendEmailToAdmin(Course $course): void
223
    {
224
        $siteName = $this->getDefaultSetting('platform.site_name');
225
        $recipientEmail = $this->getDefaultSetting('admin.administrator_email');
226
        $recipientName = $this->getDefaultSetting('admin.administrator_name').' '.$this->getDefaultSetting('admin.administrator_surname');
227
        $institutionName = $this->getDefaultSetting('platform.institution');
228
        $courseName = $course->getTitle();
229
230
        $subject = $this->translator->trans('New course created on %s')." $siteName - $institutionName";
231
232
        $greeting = $this->translator->trans('Dear %s,');
233
        $intro = $this->translator->trans('This message is to inform you that a new course has been created on %s');
234
        $courseNameLabel = $this->translator->trans('Course name');
235
236
        $message = \sprintf($greeting, $recipientName)."\n\n";
237
        $message .= "$intro $siteName - $institutionName.\n";
238
        $message .= "$courseNameLabel $courseName\n";
239
        $message .= $this->translator->trans('Course name').': '.$course->getTitle()."\n";
240
241
        foreach ($course->getCategories() as $category) {
242
            $message .= $this->translator->trans('Category').': '.$category->getCode()."\n";
243
        }
244
245
        $message .= $this->translator->trans('Coach').': '.$course->getTutorName()."\n";
246
        $message .= $this->translator->trans('Language').': '.$course->getCourseLanguage();
247
248
        $email = (new Email())
249
            ->from($recipientEmail)
250
            ->to($recipientEmail)
251
            ->subject($subject)
252
            ->text($message)
253
        ;
254
255
        $this->mailer->send($email);
256
    }
257
258
    public function defineCourseKeys(
259
        string $wantedCode,
260
        string $prefixForAll = '',
261
        string $prefixForPath = '',
262
        bool $addUniquePrefix = false,
263
        bool $useCodeIndependentKeys = true
264
    ): array {
265
        $wantedCode = $this->generateCourseCode($wantedCode);
266
        $keysCourseCode = $useCodeIndependentKeys ? $wantedCode : '';
267
268
        $uniquePrefix = $addUniquePrefix ? substr(md5(uniqid((string) rand(), true)), 0, 10) : '';
269
270
        $keys = [];
271
        $finalSuffix = ['CourseId' => '', 'CourseDir' => ''];
272
        $limitNumTry = 100;
273
        $tryCount = 0;
274
275
        $keysAreUnique = false;
276
277
        while (!$keysAreUnique && $tryCount < $limitNumTry) {
278
            $keysCourseId = $prefixForAll.$uniquePrefix.$keysCourseCode.$finalSuffix['CourseId'];
279
            $keysCourseRepository = $prefixForPath.$uniquePrefix.$wantedCode.$finalSuffix['CourseDir'];
280
281
            if ($this->courseRepository->courseCodeExists($keysCourseId)) {
282
                $finalSuffix['CourseId'] = substr(md5(uniqid((string) rand(), true)), 0, 4);
283
                $tryCount++;
284
            } else {
285
                $keysAreUnique = true;
286
            }
287
        }
288
289
        if ($keysAreUnique) {
290
            $keys = [
291
                'code' => $keysCourseCode,
292
                'visual_code' => $keysCourseId,
293
                'directory' => $keysCourseRepository,
294
            ];
295
        }
296
297
        return $keys;
298
    }
299
300
    private function fillCourse(Course $course, array $params): void
301
    {
302
        $this->debugLog('fillCourse:start', [
303
            'courseId' => $course->getId(),
304
            'exemplary_request' => !empty($params['_exemplary_content_request']),
305
        ]);
306
307
        $this->insertCourseSettings($course);
308
        $this->debugLog('fillCourse:insertCourseSettings:ok');
309
310
        $this->createGroupCategory($course);
311
        $this->debugLog('fillCourse:createGroupCategory:ok');
312
313
        $gradebook = $this->createRootGradebook($course);
314
        $this->debugLog('fillCourse:createRootGradebook:ok', ['gradebookId' => $gradebook->getId()]);
315
316
        $setting = $this->settingsManager->getSetting('course.example_material_course_creation');
317
        $this->debugLog('fillCourse:setting.example_material_course_creation', ['value' => $setting]);
318
319
        if ('true' === $setting) {
320
            $this->debugLog('fillCourse:insertExampleContent:willRun');
321
            $this->insertExampleContent($course, $gradebook);
322
            $this->debugLog('fillCourse:insertExampleContent:done');
323
        } else {
324
            $this->debugLog('fillCourse:insertExampleContent:skippedBySetting');
325
        }
326
327
        $this->entityManager->flush();
328
        $this->debugLog('fillCourse:end');
329
    }
330
331
332
333
    private function insertCourseSettings(Course $course): void
334
    {
335
        $defaultEmailExerciseAlert = 0;
336
        if ('true' === $this->settingsManager->getSetting('exercise.email_alert_manager_on_new_quiz')) {
337
            $defaultEmailExerciseAlert = 1;
338
        }
339
340
        $settings = [
341
            'email_alert_manager_on_new_doc' => ['title' => '', 'default' => 0, 'category' => 'work'],
342
            'email_alert_on_new_doc_dropbox' => ['default' => 0, 'category' => 'dropbox'],
343
            'allow_user_edit_agenda' => ['default' => 0, 'category' => 'agenda'],
344
            'allow_user_edit_announcement' => ['default' => 0, 'category' => 'announcement'],
345
            'email_alert_manager_on_new_quiz' => ['default' => $defaultEmailExerciseAlert, 'category' => 'quiz'],
346
            'allow_user_image_forum' => ['default' => 1, 'category' => 'forum'],
347
            'course_theme' => ['default' => '', 'category' => 'theme'],
348
            'allow_learning_path_theme' => ['default' => 1, 'category' => 'theme'],
349
            'allow_open_chat_window' => ['default' => 1, 'category' => 'chat'],
350
            'email_alert_to_teacher_on_new_user_in_course' => ['default' => 0, 'category' => 'registration'],
351
            'allow_user_view_user_list' => ['default' => 1, 'category' => 'user'],
352
            'display_info_advance_inside_homecourse' => ['default' => 1, 'category' => 'thematic_advance'],
353
            'email_alert_students_on_new_homework' => ['default' => 0, 'category' => 'work'],
354
            'enable_lp_auto_launch' => ['default' => 0, 'category' => 'learning_path'],
355
            'enable_exercise_auto_launch' => ['default' => 0, 'category' => 'exercise'],
356
            'enable_document_auto_launch' => ['default' => 0, 'category' => 'document'],
357
            'pdf_export_watermark_text' => ['default' => '', 'category' => 'learning_path'],
358
            'allow_public_certificates' => [
359
                'default' => 'true' === $this->settingsManager->getSetting('course.allow_public_certificates') ? 1 : '',
360
                'category' => 'certificates',
361
            ],
362
            'documents_default_visibility' => ['default' => 'visible', 'category' => 'document'],
363
            'show_course_in_user_language' => ['default' => 2, 'category' => null],
364
            'email_to_teachers_on_new_work_feedback' => ['default' => 1, 'category' => null],
365
        ];
366
367
        foreach ($settings as $variable => $setting) {
368
            $courseSetting = new CCourseSetting();
369
            $courseSetting->setCId($course->getId());
370
            $courseSetting->setVariable($variable);
371
            $courseSetting->setTitle($setting['title'] ?? '');
372
            $courseSetting->setValue((string) $setting['default']);
373
            $courseSetting->setCategory($setting['category'] ?? '');
374
375
            $this->entityManager->persist($courseSetting);
376
        }
377
378
        $this->entityManager->flush();
379
    }
380
381
    private function createGroupCategory(Course $course): void
382
    {
383
        $groupCategory = new CGroupCategory();
384
        $groupCategory
385
            ->setTitle($this->translator->trans('Default groups'))
386
            ->setParent($course)
387
            ->addCourseLink($course)
388
        ;
389
390
        $this->entityManager->persist($groupCategory);
391
        $this->entityManager->flush();
392
    }
393
394
    private function createRootGradebook(Course $course): GradebookCategory
395
    {
396
        /** @var User $currentUser */
397
        $currentUser = $this->security->getUser();
398
        if (!$currentUser instanceof User) {
399
            $currentUser = $this->getFallbackAdminUser();
400
        }
401
402
        if (!$currentUser) {
403
            throw new LogicException('There is no user currently authenticated..');
404
        }
405
406
        $gradebookCategory = new GradebookCategory();
407
        $gradebookCategory
408
            ->setTitle($course->getCode())
409
            ->setLocked(0)
410
            ->setGenerateCertificates(false)
411
            ->setDescription('')
412
            ->setCourse($course)
413
            ->setWeight(100)
414
            ->setVisible(false)
415
            ->setCertifMinScore(75)
416
            ->setUser($currentUser)
417
        ;
418
419
        $this->entityManager->persist($gradebookCategory);
420
        $this->entityManager->flush();
421
422
        return $gradebookCategory;
423
    }
424
425
    private function insertExampleContent(Course $course, GradebookCategory $gradebook): void
426
    {
427
        /** @var User $currentUser */
428
        $currentUser = $this->security->getUser();
429
        if (!$currentUser instanceof User) {
430
            $currentUser = $this->getFallbackAdminUser();
431
        }
432
433
        $docsCreated = 0;
434
        $foldersCreated = 0;
435
        $filesCreated = 0;
436
        $linksCreated = 0;
437
        $announcementId = null;
438
        $agendaAdded = false;
439
        $exerciseId = null;
440
        $forumCategoryId = null;
441
        $forumId = null;
442
        $threadId = null;
443
444
        $this->debugLog('insertExampleContent:begin', ['courseId' => $course->getId()]);
445
446
        $files = [
447
            ['path' => '/audio',          'title' => $this->translator->trans('Audio'),    'filetype' => 'folder', 'size' => 0],
448
            ['path' => '/images',         'title' => $this->translator->trans('Images'),   'filetype' => 'folder', 'size' => 0],
449
            ['path' => '/images/gallery', 'title' => $this->translator->trans('Gallery'),  'filetype' => 'folder', 'size' => 0],
450
            ['path' => '/video',          'title' => $this->translator->trans('Video'),    'filetype' => 'folder', 'size' => 0],
451
        ];
452
        $paths = [];
453
        $courseInfo = ['real_id' => $course->getId(), 'code' => $course->getCode()];
454
        foreach ($files as $file) {
455
            try {
456
                $doc = DocumentManager::addDocument(
457
                    $courseInfo,
458
                    $file['path'],
459
                    $file['filetype'],
460
                    $file['size'],
461
                    $file['title'],
462
                    null,
463
                    0,
464
                    null,
465
                    0,
466
                    0,
467
                    0,
468
                    false
469
                );
470
                $iid = method_exists($doc, 'getIid') ? $doc->getIid() : null;
471
                $paths[$file['path']] = $iid;
472
                $foldersCreated++;
473
                $docsCreated++;
474
                $this->debugLog('insertExampleContent:document:root', [
475
                    'path' => $file['path'],
476
                    'title' => $file['title'],
477
                    'iid' => $iid,
478
                ]);
479
            } catch (\Throwable $e) {
480
                $this->debugLog('insertExampleContent:document:root:error', [
481
                    'path' => $file['path'],
482
                    'msg' => $e->getMessage(),
483
                ]);
484
            }
485
        }
486
487
        $projectDir = $this->parameterBag->get('kernel.project_dir');
488
        $defaultPath = $projectDir.'/public/img/document';
489
490
        $request = $this->requestStack->getCurrentRequest();
491
        $baseUrl = $request->getSchemeAndHttpHost().$request->getBasePath();
492
493
        $finder = new Finder();
494
        if (is_dir($defaultPath)) {
495
            $finder->in($defaultPath);
496
            $countFound = 0;
497
498
            /** @var SplFileInfo $file */
499
            foreach ($finder as $file) {
500
                $countFound++;
501
                $parentName = \dirname(str_replace($defaultPath, '', $file->getRealPath()));
502
                if ('/' === $parentName || '/certificates' === $parentName) {
503
                    continue;
504
                }
505
506
                $title = $file->getFilename();
507
                $parentId = $paths[$parentName] ?? null;
508
509
                if ($file->isDir()) {
510
                    try {
511
                        $realPath = str_replace($defaultPath, '', $file->getRealPath());
512
                        $document = DocumentManager::addDocument(
513
                            $courseInfo,
514
                            $realPath,
515
                            'folder',
516
                            null,
517
                            $title,
518
                            '',
519
                            null,
520
                            null,
521
                            null,
522
                            null,
523
                            null,
524
                            false,
525
                            null,
526
                            $parentId,
527
                            $file->getRealPath()
528
                        );
529
                        $iid = method_exists($document, 'getIid') ? $document->getIid() : null;
530
                        $paths[$realPath] = $iid;
531
                        $foldersCreated++;
532
                        $docsCreated++;
533
                        $this->debugLog('insertExampleContent:document:folder', [
534
                            'path' => $realPath,
535
                            'title' => $title,
536
                            'iid' => $iid,
537
                            'parentId' => $parentId,
538
                        ]);
539
                    } catch (\Throwable $e) {
540
                        $this->debugLog('insertExampleContent:document:folder:error', [
541
                            'title' => $title,
542
                            'msg' => $e->getMessage(),
543
                        ]);
544
                    }
545
                } else {
546
                    try {
547
                        $realPath = str_replace($defaultPath, '', $file->getRealPath());
548
                        $document = DocumentManager::addDocument(
549
                            $courseInfo,
550
                            $realPath,
551
                            'file',
552
                            $file->getSize(),
553
                            $title,
554
                            '',
555
                            null,
556
                            null,
557
                            null,
558
                            null,
559
                            null,
560
                            false,
561
                            null,
562
                            $parentId,
563
                            $file->getRealPath()
564
                        );
565
                        $iid = method_exists($document, 'getIid') ? $document->getIid() : null;
566
                        $filesCreated++;
567
                        $docsCreated++;
568
                        $this->debugLog('insertExampleContent:document:file', [
569
                            'path' => $realPath,
570
                            'title' => $title,
571
                            'iid' => $iid,
572
                            'parentId' => $parentId,
573
                            'size' => $file->getSize(),
574
                        ]);
575
                    } catch (\Throwable $e) {
576
                        $this->debugLog('insertExampleContent:document:file:error', [
577
                            'title' => $title,
578
                            'msg' => $e->getMessage(),
579
                        ]);
580
                    }
581
                }
582
            }
583
584
            $this->debugLog('insertExampleContent:document:scan', [
585
                'basePath' => $defaultPath,
586
                'found' => $countFound,
587
                'foldersCreated' => $foldersCreated,
588
                'filesCreated' => $filesCreated,
589
                'totalDocsCreated' => $docsCreated,
590
            ]);
591
        } else {
592
            $this->debugLog('insertExampleContent:document:basePathMissing', ['basePath' => $defaultPath]);
593
        }
594
595
        try {
596
            $now = new DateTime('now', new DateTimeZone('UTC'));
597
            $formattedNow = $now->format('Y-m-d H:i:s');
598
599
            $agenda = new Agenda('course');
600
            $agenda->set_course($courseInfo);
601
            $agenda->addEvent(
602
                $formattedNow,
603
                $formattedNow,
604
                0,
605
                $this->translator->trans('Course creation'),
606
                $this->translator->trans('This course was created at this time'),
607
                ['everyone' => 'everyone']
608
            );
609
            $agendaAdded = true;
610
            $this->debugLog('insertExampleContent:agenda:ok');
611
        } catch (\Throwable $e) {
612
            $this->debugLog('insertExampleContent:agenda:error', ['msg' => $e->getMessage()]);
613
        }
614
615
        try {
616
            $link = new \Link();
617
            $link->setCourse($courseInfo);
618
            $links = [
619
                [
620
                    'c_id' => $course->getId(),
621
                    'url' => 'http://www.google.com',
622
                    'title' => 'Quick and powerful search engine',
623
                    'description' => $this->translator->trans('Quick and powerful search engine'),
624
                    'category_id' => 0,
625
                    'on_homepage' => 0,
626
                    'target' => '_self',
627
                    'session_id' => 0,
628
                ],
629
                [
630
                    'c_id' => $course->getId(),
631
                    'url' => 'http://www.wikipedia.org',
632
                    'title' => 'Free online encyclopedia',
633
                    'description' => $this->translator->trans('Free online encyclopedia'),
634
                    'category_id' => 0,
635
                    'on_homepage' => 0,
636
                    'target' => '_self',
637
                    'session_id' => 0,
638
                ],
639
            ];
640
641
            foreach ($links as $params) {
642
                $link->save($params, false, false);
643
                $linksCreated++;
644
            }
645
            $this->debugLog('insertExampleContent:links:ok', ['linksCreated' => $linksCreated]);
646
        } catch (\Throwable $e) {
647
            $this->debugLog('insertExampleContent:links:error', ['msg' => $e->getMessage()]);
648
        }
649
650
        try {
651
            $announcementId = \AnnouncementManager::add_announcement(
652
                $courseInfo,
653
                0,
654
                $this->translator->trans('This is an announcement example'),
655
                $this->translator->trans('This is an announcement example. Only trainers are allowed to publish announcements.'),
656
                ['everyone' => 'everyone'],
657
                null,
658
                null,
659
                (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s')
660
            );
661
            $this->debugLog('insertExampleContent:announcement:ok', ['announcementId' => $announcementId]);
662
        } catch (\Throwable $e) {
663
            $this->debugLog('insertExampleContent:announcement:error', ['msg' => $e->getMessage()]);
664
        }
665
666
        try {
667
            $exercise = new \Exercise($course->getId());
668
            $exercise->exercise = $this->translator->trans('Sample test');
669
            $html = '<table width="100%" border="0" cellpadding="0" cellspacing="0">
670
                        <tr>
671
                        <td width="220" valign="top" align="left">
672
                            <img src="'.$baseUrl.'/public/img/document/images/mr_chamilo/doubts.png">
673
                        </td>
674
                        <td valign="top" align="left">'.$this->translator->trans('Irony').'</td></tr>
675
                    </table>';
676
            $exercise->type = 1;
677
            $exercise->setRandom(0);
678
            $exercise->active = 1;
679
            $exercise->results_disabled = 0;
680
            $exercise->description = $html;
681
            $exercise->save();
682
            $exerciseId = $exercise->id;
683
            $this->debugLog('insertExampleContent:exercise:created', ['exerciseId' => $exerciseId]);
684
685
            $question = new \MultipleAnswer();
686
            $question->course = $courseInfo;
687
            $question->question = $this->translator->trans('Socratic irony is...');
688
            $question->description = $this->translator->trans('(more than one answer can be true)');
689
            $question->weighting = 10;
690
            $question->position = 1;
691
            $question->course = $courseInfo;
692
            $question->save($exercise);
693
            $questionId = $question->id;
694
695
            $answer = new \Answer($questionId, $courseInfo['real_id']);
696
            $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);
697
            $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);
698
            $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. Socratic irony is an interrogative method. The Greek "eirotao" means "ask questions"'), 5, 3);
699
            $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);
700
            $answer->save();
701
            $this->debugLog('insertExampleContent:exercise:questionAnswers:ok', ['questionId' => $questionId]);
702
703
            // Gradebook link
704
            $manager = $this->entityManager;
705
            $childGradebookCategory = new GradebookCategory();
706
            $childGradebookCategory->setTitle($course->getCode());
707
            $childGradebookCategory->setLocked(0);
708
            $childGradebookCategory->setGenerateCertificates(false);
709
            $childGradebookCategory->setDescription('');
710
            $childGradebookCategory->setCourse($course);
711
            $childGradebookCategory->setWeight(100);
712
            $childGradebookCategory->setVisible(true);
713
            $childGradebookCategory->setCertifMinScore(75);
714
            $childGradebookCategory->setParent($gradebook);
715
            $childGradebookCategory->setUser(api_get_user_entity());
716
717
            $manager->persist($childGradebookCategory);
718
            $manager->flush();
719
720
            $gradebookLink = new GradebookLink();
721
            $gradebookLink->setType(1);
722
            $gradebookLink->setRefId($exerciseId);
723
            $gradebookLink->setUserScoreList([]);
724
            $gradebookLink->setCourse($course);
725
            $gradebookLink->setCategory($childGradebookCategory);
726
            $gradebookLink->setCreatedAt(new DateTime());
727
            $gradebookLink->setWeight(100);
728
            $gradebookLink->setVisible(1);
729
            $gradebookLink->setLocked(0);
730
731
            $manager->persist($gradebookLink);
732
            $manager->flush();
733
            $this->debugLog('insertExampleContent:gradebookLink:ok', [
734
                'childCategoryId' => $childGradebookCategory->getId(),
735
                'refId' => $exerciseId,
736
            ]);
737
        } catch (\Throwable $e) {
738
            $this->debugLog('insertExampleContent:exercise:error', ['msg' => $e->getMessage()]);
739
        }
740
741
        try {
742
            $params = [
743
                'forum_category_title' => $this->translator->trans('Example Forum Category'),
744
                'forum_category_comment' => '',
745
            ];
746
            $forumCategoryId = \saveForumCategory($params, $courseInfo, false);
747
            $this->debugLog('insertExampleContent:forum:category', ['forumCategoryId' => $forumCategoryId]);
748
749
            $params = [
750
                'forum_category' => $forumCategoryId,
751
                'forum_title' => $this->translator->trans('Example Forum'),
752
                'forum_comment' => '',
753
                'default_view_type_group' => ['default_view_type' => 'flat'],
754
            ];
755
            $forumId = \store_forum($params, $courseInfo, true);
756
            $this->debugLog('insertExampleContent:forum:forum', ['forumId' => $forumId]);
757
758
            $repo = $this->entityManager->getRepository(CForum::class);
759
            $forumEntity = $repo->find($forumId);
760
761
            $params = [
762
                'post_title' => $this->translator->trans('Example Thread'),
763
                'forum_id' => $forumId,
764
                'post_text' => $this->translator->trans('Example content'),
765
                'calification_notebook_title' => '',
766
                'numeric_calification' => '',
767
                'weight_calification' => '',
768
                'forum_category' => $forumCategoryId,
769
                'thread_peer_qualify' => 0,
770
            ];
771
            $threadId = \saveThread($forumEntity, $params, $courseInfo, false);
772
            $this->debugLog('insertExampleContent:forum:thread', ['threadId' => $threadId]);
773
        } catch (\Throwable $e) {
774
            $this->debugLog('insertExampleContent:forum:error', ['msg' => $e->getMessage()]);
775
        }
776
777
        $this->debugLog('insertExampleContent:end', [
778
            'courseId' => $course->getId(),
779
            'docsCreated' => $docsCreated,
780
            'foldersCreated' => $foldersCreated,
781
            'filesCreated' => $filesCreated,
782
            'linksCreated' => $linksCreated,
783
            'agendaAdded' => $agendaAdded,
784
            'announcementId' => $announcementId,
785
            'exerciseId' => $exerciseId,
786
            'forumCategoryId' => $forumCategoryId,
787
            'forumId' => $forumId,
788
            'threadId' => $threadId,
789
        ]);
790
    }
791
792
    private function createExampleGradebookContent(Course $course, GradebookCategory $parentCategory, int $refId): void
793
    {
794
        $manager = $this->entityManager;
795
796
        /* Gradebook tool */
797
        $courseCode = $course->getCode();
798
799
        $childGradebookCategory = new GradebookCategory();
800
        $childGradebookCategory->setTitle($courseCode);
801
        $childGradebookCategory->setLocked(0);
802
        $childGradebookCategory->setGenerateCertificates(false);
803
        $childGradebookCategory->setDescription('');
804
        $childGradebookCategory->setCourse($course);
805
        $childGradebookCategory->setWeight(100);
806
        $childGradebookCategory->setVisible(true);
807
        $childGradebookCategory->setCertifMinScore(75);
808
        $childGradebookCategory->setParent($parentCategory);
809
        $childGradebookCategory->setUser(api_get_user_entity());
810
811
        $manager->persist($childGradebookCategory);
812
        $manager->flush();
813
814
        $gradebookLink = new GradebookLink();
815
816
        $gradebookLink->setType(1);
817
        $gradebookLink->setRefId($refId);
818
        $gradebookLink->setUserScoreList([]);
819
        $gradebookLink->setCourse($course);
820
        $gradebookLink->setCategory($childGradebookCategory);
821
        $gradebookLink->setCreatedAt(new DateTime());
822
        $gradebookLink->setWeight(100);
823
        $gradebookLink->setVisible(1);
824
        $gradebookLink->setLocked(0);
825
826
        $manager->persist($gradebookLink);
827
        $manager->flush();
828
    }
829
830
    private function installCoursePlugins(int $courseId): void
831
    {
832
        $app_plugin = new AppPlugin();
833
        $app_plugin->install_course_plugins($courseId);
834
    }
835
836
    public function useTemplateAsBasisIfRequired($courseCode, $courseTemplate): void
837
    {
838
        $templateSetting = $this->settingsManager->getSetting('course.course_creation_use_template');
839
        $teacherCanSelectCourseTemplate = 'true' === $this->settingsManager->getSetting('course.teacher_can_select_course_template');
840
        $courseTemplate = isset($courseTemplate) ? (int) $courseTemplate : 0;
841
842
        $useTemplate = false;
843
        if ($teacherCanSelectCourseTemplate && $courseTemplate > 0) {
844
            $useTemplate = true;
845
            $originCourse = $this->courseRepository->findCourseAsArray((int) $courseTemplate);
846
        } elseif (!empty($templateSetting)) {
847
            $useTemplate = true;
848
            $originCourse = $this->courseRepository->findCourseAsArray((int) $templateSetting);
849
        }
850
851
        if ($useTemplate && !empty($originCourse)) {
852
            try {
853
                $originCourse['official_code'] = $originCourse['code'];
854
                $cb = new CourseBuilder(null, $originCourse);
855
                $course = $cb->build(null, $originCourse['code']);
856
                $cr = new CourseRestorer($course);
857
                $cr->set_file_option();
858
                $cr->restore($courseCode);
859
            } catch (Exception $e) {
860
                error_log('Error during course template application: '.$e->getMessage());
861
            } catch (Throwable $t) {
862
                error_log('Unexpected error during course template application: '.$t->getMessage());
863
            }
864
        }
865
    }
866
867
    private function prepareAndValidateCourseData(array $params): array
868
    {
869
        $title = str_replace('&amp;', '&', $params['title'] ?? '');
870
        $code = $params['code'] ?? '';
871
        $visualCode = $params['visual_code'] ?? '';
872
        $directory = $params['directory'] ?? '';
873
        $tutorName = $params['tutor_name'] ?? null;
874
        $courseLanguage = !empty($params['course_language']) ? $params['course_language'] : $this->getDefaultSetting('language.platform_language');
875
        $departmentName = $params['department_name'] ?? null;
876
        $departmentUrl = $this->fixDepartmentUrl($params['department_url'] ?? '');
877
        $diskQuota = $params['disk_quota'] ?? $this->getDefaultSetting('document.default_document_quotum');
878
        $visibility = $params['visibility'] ?? $this->getDefaultSetting('course.courses_default_creation_visibility', Course::OPEN_PLATFORM);
879
        $subscribe = $params['subscribe'] ?? (Course::OPEN_PLATFORM == $visibility);
880
        $unsubscribe = $params['unsubscribe'] ?? false;
881
        $expirationDate = $params['expiration_date'] ?? $this->getFutureExpirationDate();
882
        $teachers = $params['teachers'] ?? [];
883
        $categories = $params['course_categories'] ?? [];
884
        $notifyAdmins = $this->getDefaultSetting('course.send_email_to_admin_when_create_course');
885
886
        $errors = [];
887
        if (empty($code)) {
888
            $errors[] = 'courseSysCode is missing';
889
        }
890
        if (empty($visualCode)) {
891
            $errors[] = 'courseScreenCode is missing';
892
        }
893
        if (empty($directory)) {
894
            $errors[] = 'courseRepository is missing';
895
        }
896
        if (empty($title)) {
897
            $errors[] = 'title is missing';
898
        }
899
        if ($visibility < 0 || $visibility > 4) {
900
            $errors[] = 'visibility is invalid';
901
        }
902
903
        if (!empty($errors)) {
904
            throw new Exception(implode(', ', $errors));
905
        }
906
907
        return [
908
            'title' => $title,
909
            'code' => $code,
910
            'visualCode' => $visualCode,
911
            'directory' => $directory,
912
            'tutorName' => $tutorName,
913
            'courseLanguage' => $courseLanguage,
914
            'departmentName' => $departmentName,
915
            'departmentUrl' => $departmentUrl,
916
            'diskQuota' => $diskQuota,
917
            'visibility' => $visibility,
918
            'subscribe' => $subscribe,
919
            'unsubscribe' => $unsubscribe,
920
            'expirationDate' => new DateTime($expirationDate),
921
            'teachers' => $teachers,
922
            'categories' => $categories,
923
            'notifyAdmins' => $notifyAdmins,
924
        ];
925
    }
926
927
    private function getDefaultSetting(string $name, $default = null)
928
    {
929
        $settingValue = $this->settingsManager->getSetting($name);
930
931
        return null !== $settingValue ? $settingValue : $default;
932
    }
933
934
    private function fixDepartmentUrl(string $url): string
935
    {
936
        if (!empty($url) && !str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
937
            return 'http://'.$url;
938
        }
939
940
        return 'http://' === $url ? '' : $url;
941
    }
942
943
    private function getFutureExpirationDate(): string
944
    {
945
        return (new DateTime())->modify('+1 year')->format('Y-m-d H:i:s');
946
    }
947
948
    private function generateCourseCode(string $title): string
949
    {
950
        $cleanTitle = preg_replace('/[^A-Z0-9]/', '', strtoupper($this->replaceDangerousChar($title)));
951
952
        return substr($cleanTitle, 0, self::MAX_COURSE_LENGTH_CODE);
953
    }
954
955
    private function replaceDangerousChar(string $text): string
956
    {
957
        $encoding = mb_detect_encoding($text, mb_detect_order(), true);
958
        if ('UTF-8' !== $encoding) {
959
            $text = mb_convert_encoding($text, 'UTF-8', $encoding);
960
        }
961
962
        $text = str_replace(' ', '-', $text);
963
        $text = preg_replace('/[^-\w]+/', '', $text);
964
965
        return preg_replace('/\.+$/', '', $text);
966
    }
967
968
    private function handlePostCourseCreation(Course $course, array $params): void
969
    {
970
        if ($params['notifyAdmins'] ?? false) {
971
            $this->sendEmailToAdmin($course);
972
        }
973
974
        $currentUser = $this->security->getUser();
975
        if (!$currentUser instanceof User) {
976
            $currentUser = $this->getFallbackAdminUser();
977
        }
978
979
        $this->eventLoggerHelper->addEvent(
980
            'course_created',
981
            'course_id',
982
            $course->getId(),
983
            null,
984
            $currentUser->getId(),
985
            $course->getId()
986
        );
987
    }
988
989
    private function getFallbackAdminUser(): User
990
    {
991
        $qb = $this->entityManager->createQueryBuilder();
992
993
        $qb->select('u')
994
            ->from(User::class, 'u')
995
            ->where('u.roles LIKE :role')
996
            ->setParameter('role', '%ROLE_ADMIN%')
997
            ->setMaxResults(1)
998
        ;
999
1000
        $user = $qb->getQuery()->getOneOrNullResult();
1001
1002
        if (!$user instanceof User) {
1003
            throw new RuntimeException('No admin user found for fallback.');
1004
        }
1005
1006
        return $user;
1007
    }
1008
1009
    private function debugLog(string $message, array $context = []): void
1010
    {
1011
        if (!$this->debug) {
1012
            return;
1013
        }
1014
        $suffix = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) : '';
1015
        error_log('[CourseHelper] ' . $message . $suffix);
1016
    }
1017
}
1018