Passed
Pull Request — master (#6935)
by
unknown
08:44
created

CourseBuilder   F

Complexity

Total Complexity 286

Size/Duplication

Total Lines 2220
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 1185
c 0
b 0
f 0
dl 0
loc 2220
rs 0.86
wmc 286

42 Methods

Rating   Name   Duplication   Size   Complexity  
A build_forums() 0 40 4
A __construct() 0 16 2
A set_tools_to_build() 0 3 1
A resolveNodeBySegments() 0 12 3
C restoreDocumentsFromList() 0 56 13
A get_course() 0 3 1
A addDocumentList() 0 5 3
A extractDocumentSegmentsFromUrl() 0 14 3
A set_tools_specific_id_list() 0 3 1
A findAndSetDocumentsInText() 0 7 2
B build_works() 0 64 9
A build_glossary() 0 37 4
A addAsset() 0 8 3
B build_documents_with_repo() 0 66 11
A tryAddAsset() 0 6 3
A build_forum_category() 0 28 3
F mkLegacyItem() 0 72 34
B build_forum_posts() 0 51 8
D build_learnpaths() 0 104 17
B exportQuestionsWithAnswers() 0 67 5
A build_documents() 0 13 2
A build_learnpath_category() 0 33 4
B build_quizzes() 0 98 7
A build_quiz_questions() 0 42 5
B build_thematic() 0 84 9
C build_events() 0 97 17
A build_forum_topics() 0 35 5
B build_tool_intro() 0 64 11
A fmtDate() 0 3 2
A build_course_descriptions() 0 38 4
C build_announcements() 0 99 12
B build_survey_questions() 0 75 7
A makeIdFilter() 0 8 2
A build_gradebook() 0 31 5
F build() 0 134 29
B serializeGradebookCategory() 0 68 5
A build_wiki() 0 50 4
A build_link_category() 0 15 2
B build_surveys() 0 86 8
A safeCount() 0 3 3
B build_attendance() 0 58 6
B build_links() 0 52 7

How to fix   Complexity   

Complex Class

Complex classes like CourseBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CourseBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CourseBundle\Component\CourseCopy;
8
9
use Chamilo\CoreBundle\Entity\Course as CourseEntity;
10
use Chamilo\CoreBundle\Entity\GradebookCategory;
11
use Chamilo\CoreBundle\Entity\GradebookEvaluation;
12
use Chamilo\CoreBundle\Entity\GradebookLink;
13
use Chamilo\CoreBundle\Entity\ResourceFile;
14
use Chamilo\CoreBundle\Entity\ResourceNode;
15
use Chamilo\CoreBundle\Entity\Session as SessionEntity;
16
use Chamilo\CoreBundle\Framework\Container;
17
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
18
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Attendance;
19
use Chamilo\CourseBundle\Component\CourseCopy\Resources\CalendarEvent;
20
use Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseDescription;
21
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Document;
22
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Glossary;
23
use Chamilo\CourseBundle\Component\CourseCopy\Resources\GradeBookBackup;
24
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Thematic;
25
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Wiki;
26
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Work;
27
use Chamilo\CourseBundle\Entity\CAnnouncement;
28
use Chamilo\CourseBundle\Entity\CAnnouncementAttachment;
29
use Chamilo\CourseBundle\Entity\CAttendance;
30
use Chamilo\CourseBundle\Entity\CAttendanceCalendar;
31
use Chamilo\CourseBundle\Entity\CCalendarEvent;
32
use Chamilo\CourseBundle\Entity\CCalendarEventAttachment;
33
use Chamilo\CourseBundle\Entity\CCourseDescription;
34
use Chamilo\CourseBundle\Entity\CDocument;
35
use Chamilo\CourseBundle\Entity\CForum;
36
use Chamilo\CourseBundle\Entity\CForumCategory;
37
use Chamilo\CourseBundle\Entity\CForumPost;
38
use Chamilo\CourseBundle\Entity\CForumThread;
39
use Chamilo\CourseBundle\Entity\CGlossary;
40
use Chamilo\CourseBundle\Entity\CLink;
41
use Chamilo\CourseBundle\Entity\CLinkCategory;
42
use Chamilo\CourseBundle\Entity\CLp;
43
use Chamilo\CourseBundle\Entity\CLpCategory;
44
use Chamilo\CourseBundle\Entity\CLpItem;
45
use Chamilo\CourseBundle\Entity\CQuiz;
46
use Chamilo\CourseBundle\Entity\CQuizAnswer;
47
use Chamilo\CourseBundle\Entity\CQuizQuestion;
48
use Chamilo\CourseBundle\Entity\CQuizQuestionOption;
49
use Chamilo\CourseBundle\Entity\CQuizRelQuestion;
50
use Chamilo\CourseBundle\Entity\CStudentPublication;
51
use Chamilo\CourseBundle\Entity\CSurvey;
52
use Chamilo\CourseBundle\Entity\CSurveyQuestion;
53
use Chamilo\CourseBundle\Entity\CSurveyQuestionOption;
54
use Chamilo\CourseBundle\Entity\CThematic;
55
use Chamilo\CourseBundle\Entity\CThematicAdvance;
56
use Chamilo\CourseBundle\Entity\CThematicPlan;
57
use Chamilo\CourseBundle\Entity\CTool;
58
use Chamilo\CourseBundle\Entity\CToolIntro;
59
use Chamilo\CourseBundle\Entity\CWiki;
60
use Chamilo\CourseBundle\Repository\CDocumentRepository;
61
use Closure;
62
use Countable;
63
use Database;
64
use DateTimeInterface;
65
use Doctrine\Common\Collections\Collection;
66
use Doctrine\ORM\EntityManagerInterface;
67
use DocumentManager;
68
use ReflectionProperty;
69
use stdClass;
70
use Symfony\Component\HttpKernel\KernelInterface;
71
use Throwable;
72
73
/**
74
 * CourseBuilder focused on Doctrine/ResourceNode export (keeps legacy orchestration).
75
 */
76
class CourseBuilder
77
{
78
    /**
79
     * @var Course Legacy course container used by the exporter
80
     */
81
    public $course;
82
83
    /**
84
     * @var array<string> Only the tools to build (defaults kept)
85
     */
86
    public array $tools_to_build = [
87
        'documents', 'forums', 'tool_intro', 'links', 'quizzes', 'quiz_questions',
88
        'assets', 'surveys', 'survey_questions', 'announcements', 'events',
89
        'course_descriptions', 'glossary', 'wiki', 'thematic', 'attendance', 'works',
90
        'gradebook', 'learnpath_category', 'learnpaths',
91
    ];
92
93
    /**
94
     * @var array<string, int|string> Legacy constant map (extend as you add tools)
95
     */
96
    public array $toolToName = [
97
        'documents' => RESOURCE_DOCUMENT,
98
        'forums' => RESOURCE_FORUM,
99
        'tool_intro' => RESOURCE_TOOL_INTRO,
100
        'links' => RESOURCE_LINK,
101
        'quizzes' => RESOURCE_QUIZ,
102
        'quiz_questions' => RESOURCE_QUIZQUESTION,
103
        'assets' => 'asset',
104
        'surveys' => RESOURCE_SURVEY,
105
        'survey_questions' => RESOURCE_SURVEYQUESTION,
106
        'announcements' => RESOURCE_ANNOUNCEMENT,
107
        'events' => RESOURCE_EVENT,
108
        'course_descriptions' => RESOURCE_COURSEDESCRIPTION,
109
        'glossary' => RESOURCE_GLOSSARY,
110
        'wiki' => RESOURCE_WIKI,
111
        'thematic' => RESOURCE_THEMATIC,
112
        'attendance' => RESOURCE_ATTENDANCE,
113
        'works' => RESOURCE_WORK,
114
        'gradebook' => RESOURCE_GRADEBOOK,
115
        'learnpaths' => RESOURCE_LEARNPATH,
116
        'learnpath_category' => RESOURCE_LEARNPATH_CATEGORY,
117
    ];
118
119
    /**
120
     * @var array<string, array<int>> Optional whitelist of IDs per tool
121
     */
122
    public array $specific_id_list = [];
123
124
    /**
125
     * @var array<int, array{0:string,1:string,2:string}> Documents referenced inside HTML
126
     */
127
    public array $documentsAddedInText = [];
128
129
    /**
130
     * Doctrine services.
131
     */
132
    private $em;       // Doctrine EntityManager
133
    private $docRepo;  // CDocumentRepository
134
135
    /**
136
     * Constructor (keeps legacy init; wires Doctrine repositories).
137
     *
138
     * @param string     $type   'partial'|'complete'
139
     * @param array|null $course Optional course info array
140
     */
141
    public function __construct($type = '', $course = null)
142
    {
143
        // Legacy behavior preserved
144
        $_course = api_get_course_info();
145
        if (!empty($course['official_code'])) {
146
            $_course = $course;
147
        }
148
149
        $this->course = new Course();
150
        $this->course->code = $_course['code'];
151
        $this->course->type = $type;
152
        $this->course->encoding = api_get_system_encoding();
153
        $this->course->info = $_course;
154
155
        $this->em = Database::getManager();
156
        $this->docRepo = Container::getDocumentRepository();
157
158
        // Use $this->em / $this->docRepo in build_documents() when needed.
159
    }
160
161
    /**
162
     * Merge a parsed list of document refs into memory.
163
     *
164
     * @param array<int, array{0:string,1:string,2:string}> $list
165
     */
166
    public function addDocumentList(array $list): void
167
    {
168
        foreach ($list as $item) {
169
            if (!\in_array($item[0], $this->documentsAddedInText, true)) {
170
                $this->documentsAddedInText[$item[0]] = $item;
171
            }
172
        }
173
    }
174
175
    /**
176
     * Parse HTML and collect referenced course documents.
177
     *
178
     * @param string $html HTML content
179
     */
180
    public function findAndSetDocumentsInText(string $html = ''): void
181
    {
182
        if ('' === $html) {
183
            return;
184
        }
185
        $documentList = DocumentManager::get_resources_from_source_html($html);
186
        $this->addDocumentList($documentList);
187
    }
188
189
    /**
190
     * Resolve collected HTML links to CDocument iids via the ResourceNode tree and build them.
191
     */
192
    public function restoreDocumentsFromList(): void
193
    {
194
        if (empty($this->documentsAddedInText)) {
195
            return;
196
        }
197
198
        $courseInfo = api_get_course_info();
199
        $courseCode = (string) ($courseInfo['code'] ?? '');
200
        if ('' === $courseCode) {
201
            return;
202
        }
203
204
        /** @var CourseEntity|null $course */
205
        $course = $this->em->getRepository(CourseEntity::class)->findOneBy(['code' => $courseCode]);
206
        if (!$course instanceof CourseEntity) {
207
            return;
208
        }
209
210
        // Documents root under the course
211
        $root = $this->docRepo->getCourseDocumentsRootNode($course);
212
        if (!$root instanceof ResourceNode) {
213
            return;
214
        }
215
216
        $iids = [];
217
218
        foreach ($this->documentsAddedInText as $item) {
219
            [$url, $scope, $type] = $item; // url, scope(local/remote), type(rel/abs/url)
220
            if ('local' !== $scope || !\in_array($type, ['rel', 'abs'], true)) {
221
                continue;
222
            }
223
224
            $segments = $this->extractDocumentSegmentsFromUrl((string) $url);
225
            if (!$segments) {
226
                continue;
227
            }
228
229
            // Walk the ResourceNode tree by matching child titles
230
            $node = $this->resolveNodeBySegments($root, $segments);
231
            if (!$node) {
232
                continue;
233
            }
234
235
            $resource = $this->docRepo->getResourceByResourceNode($node);
236
            if ($resource instanceof CDocument && \is_int($resource->getIid())) {
237
                $iids[] = $resource->getIid();
238
            }
239
        }
240
241
        $iids = array_values(array_unique($iids));
242
        if ($iids) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $iids of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
243
            $this->build_documents(
244
                api_get_session_id(),
245
                (int) $course->getId(),
246
                true,
247
                $iids
248
            );
249
        }
250
    }
251
252
    /**
253
     * Extract path segments after "/document".
254
     *
255
     * @return array<string>
256
     */
257
    private function extractDocumentSegmentsFromUrl(string $url): array
258
    {
259
        $decoded = urldecode($url);
260
        if (!preg_match('#/document(/.*)$#', $decoded, $m)) {
261
            return [];
262
        }
263
        $tail = trim($m[1], '/'); // e.g. "Folder/Sub/file.pdf"
264
        if ('' === $tail) {
265
            return [];
266
        }
267
268
        $parts = array_values(array_filter(explode('/', $tail), static fn ($s) => '' !== $s));
269
270
        return array_map(static fn ($s) => trim($s), $parts);
271
    }
272
273
    /**
274
     * Walk children by title from a given parent node.
275
     *
276
     * @param array<int,string> $segments
277
     */
278
    private function resolveNodeBySegments(ResourceNode $parent, array $segments): ?ResourceNode
279
    {
280
        $node = $parent;
281
        foreach ($segments as $title) {
282
            $child = $this->docRepo->findChildNodeByTitle($node, $title);
283
            if (!$child instanceof ResourceNode) {
284
                return null;
285
            }
286
            $node = $child;
287
        }
288
289
        return $node;
290
    }
291
292
    /**
293
     * Set tools to build.
294
     *
295
     * @param array<string> $array
296
     */
297
    public function set_tools_to_build(array $array): void
298
    {
299
        $this->tools_to_build = $array;
300
    }
301
302
    /**
303
     * Set specific id list per tool.
304
     *
305
     * @param array<string, array<int>> $array
306
     */
307
    public function set_tools_specific_id_list(array $array): void
308
    {
309
        $this->specific_id_list = $array;
310
    }
311
312
    /**
313
     * Get legacy Course container.
314
     */
315
    public function get_course(): Course
316
    {
317
        return $this->course;
318
    }
319
320
    /**
321
     * Build the course (documents already repo-based; other tools preserved).
322
     *
323
     * @param array<int|string>   $parseOnlyToolList
324
     * @param array<string,mixed> $toolsFromPost
325
     */
326
    public function build(
327
        int $session_id = 0,
328
        string $courseCode = '',
329
        bool $withBaseContent = false,
330
        array $parseOnlyToolList = [],
331
        array $toolsFromPost = []
332
    ): Course {
333
        /** @var CourseEntity|null $courseEntity */
334
        $courseEntity = '' !== $courseCode
335
            ? $this->em->getRepository(CourseEntity::class)->findOneBy(['code' => $courseCode])
336
            : $this->em->getRepository(CourseEntity::class)->find(api_get_course_int_id());
337
338
        /** @var SessionEntity|null $sessionEntity */
339
        $sessionEntity = $session_id
340
            ? $this->em->getRepository(SessionEntity::class)->find($session_id)
341
            : null;
342
343
        // Legacy DTO where resources[...] are built
344
        $legacyCourse = $this->course;
345
        foreach ($this->tools_to_build as $toolKey) {
346
            if (!empty($parseOnlyToolList)) {
347
                $const = $this->toolToName[$toolKey] ?? null;
348
                if (null !== $const && !\in_array($const, $parseOnlyToolList, true)) {
349
                    continue;
350
                }
351
            }
352
353
            if ('documents' === $toolKey) {
354
                $ids = $this->specific_id_list['documents'] ?? [];
355
                $this->build_documents_with_repo($courseEntity, $sessionEntity, $withBaseContent, $ids);
356
            }
357
358
            if ('forums' === $toolKey || 'forum' === $toolKey) {
359
                $ids = $this->specific_id_list['forums'] ?? $this->specific_id_list['forum'] ?? [];
360
                $this->build_forum_category($legacyCourse, $courseEntity, $sessionEntity, $ids);
361
                $this->build_forums($legacyCourse, $courseEntity, $sessionEntity, $ids);
362
                $this->build_forum_topics($legacyCourse, $courseEntity, $sessionEntity, $ids);
363
                $this->build_forum_posts($legacyCourse, $courseEntity, $sessionEntity, $ids);
364
            }
365
366
            if ('tool_intro' === $toolKey) {
367
                $this->build_tool_intro($legacyCourse, $courseEntity, $sessionEntity);
368
            }
369
370
            if ('links' === $toolKey) {
371
                $ids = $this->specific_id_list['links'] ?? [];
372
                $this->build_links($legacyCourse, $courseEntity, $sessionEntity, $ids);
373
            }
374
375
            if ('quizzes' === $toolKey || 'quiz' === $toolKey) {
376
                $ids = $this->specific_id_list['quizzes'] ?? $this->specific_id_list['quiz'] ?? [];
377
                $neededQuestionIds = $this->build_quizzes($legacyCourse, $courseEntity, $sessionEntity, $ids);
378
                // Always export question bucket required by the quizzes
379
                $this->build_quiz_questions($legacyCourse, $courseEntity, $sessionEntity, $neededQuestionIds);
380
                error_log(
381
                    'COURSE_BUILD: quizzes='.\count($legacyCourse->resources[RESOURCE_QUIZ] ?? []).
382
                    ' quiz_questions='.\count($legacyCourse->resources[RESOURCE_QUIZQUESTION] ?? [])
383
                );
384
            }
385
386
            if ('quiz_questions' === $toolKey) {
387
                $ids = $this->specific_id_list['quiz_questions'] ?? [];
388
                $this->build_quiz_questions($legacyCourse, $courseEntity, $sessionEntity, $ids);
389
                error_log(
390
                    'COURSE_BUILD: explicit quiz_questions='.\count($legacyCourse->resources[RESOURCE_QUIZQUESTION] ?? [])
391
                );
392
            }
393
394
            if ('surveys' === $toolKey || 'survey' === $toolKey) {
395
                $ids = $this->specific_id_list['surveys'] ?? $this->specific_id_list['survey'] ?? [];
396
                $neededQ = $this->build_surveys($this->course, $courseEntity, $sessionEntity, $ids);
397
                $this->build_survey_questions($this->course, $courseEntity, $sessionEntity, $neededQ);
398
            }
399
400
            if ('survey_questions' === $toolKey) {
401
                $this->build_survey_questions($this->course, $courseEntity, $sessionEntity, []);
402
            }
403
404
            if ('announcements' === $toolKey) {
405
                $ids = $this->specific_id_list['announcements'] ?? [];
406
                $this->build_announcements($this->course, $courseEntity, $sessionEntity, $ids);
407
            }
408
409
            if ('events' === $toolKey) {
410
                $ids = $this->specific_id_list['events'] ?? [];
411
                $this->build_events($this->course, $courseEntity, $sessionEntity, $ids);
412
            }
413
414
            if ('course_descriptions' === $toolKey) {
415
                $ids = $this->specific_id_list['course_descriptions'] ?? [];
416
                $this->build_course_descriptions($this->course, $courseEntity, $sessionEntity, $ids);
417
            }
418
419
            if ('glossary' === $toolKey) {
420
                $ids = $this->specific_id_list['glossary'] ?? [];
421
                $this->build_glossary($this->course, $courseEntity, $sessionEntity, $ids);
422
            }
423
424
            if ('wiki' === $toolKey) {
425
                $ids = $this->specific_id_list['wiki'] ?? [];
426
                $this->build_wiki($this->course, $courseEntity, $sessionEntity, $ids);
427
            }
428
429
            if ('thematic' === $toolKey) {
430
                $ids = $this->specific_id_list['thematic'] ?? [];
431
                $this->build_thematic($this->course, $courseEntity, $sessionEntity, $ids);
432
            }
433
434
            if ('attendance' === $toolKey) {
435
                $ids = $this->specific_id_list['attendance'] ?? [];
436
                $this->build_attendance($this->course, $courseEntity, $sessionEntity, $ids);
437
            }
438
439
            if ('works' === $toolKey) {
440
                $ids = $this->specific_id_list['works'] ?? [];
441
                $this->build_works($this->course, $courseEntity, $sessionEntity, $ids);
442
            }
443
444
            if ('gradebook' === $toolKey) {
445
                $this->build_gradebook($this->course, $courseEntity, $sessionEntity);
446
            }
447
448
            if ('learnpath_category' === $toolKey) {
449
                $ids = $this->specific_id_list['learnpath_category'] ?? [];
450
                $this->build_learnpath_category($this->course, $courseEntity, $sessionEntity, $ids);
451
            }
452
453
            if ('learnpaths' === $toolKey) {
454
                $ids = $this->specific_id_list['learnpaths'] ?? [];
455
                $this->build_learnpaths($this->course, $courseEntity, $sessionEntity, $ids, true);
456
            }
457
        }
458
459
        return $this->course;
460
    }
461
462
    /**
463
     * Export Learnpath categories (CLpCategory).
464
     *
465
     * @param array<int> $ids
466
     */
467
    private function build_learnpath_category(
468
        object $legacyCourse,
469
        ?CourseEntity $courseEntity,
470
        ?SessionEntity $sessionEntity,
471
        array $ids
472
    ): void {
473
        if (!$courseEntity instanceof CourseEntity) {
474
            return;
475
        }
476
477
        $repo = Container::getLpCategoryRepository();
478
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
479
480
        if (!empty($ids)) {
481
            $qb->andWhere('resource.iid IN (:ids)')
482
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
483
            ;
484
        }
485
486
        /** @var CLpCategory[] $rows */
487
        $rows = $qb->getQuery()->getResult();
488
489
        foreach ($rows as $cat) {
490
            $iid = (int) $cat->getIid();
491
            $title = (string) $cat->getTitle();
492
493
            $payload = [
494
                'id' => $iid,
495
                'title' => $title,
496
            ];
497
498
            $legacyCourse->resources[RESOURCE_LEARNPATH_CATEGORY][$iid] =
499
                $this->mkLegacyItem(RESOURCE_LEARNPATH_CATEGORY, $iid, $payload);
500
        }
501
    }
502
503
    /**
504
     * Export Learnpaths (CLp) + items, with optional SCORM folder packing.
505
     *
506
     * @param array<int> $idList
507
     */
508
    private function build_learnpaths(
509
        object $legacyCourse,
510
        ?CourseEntity $courseEntity,
511
        ?SessionEntity $sessionEntity,
512
        array $idList = [],
513
        bool $addScormFolder = true
514
    ): void {
515
        if (!$courseEntity instanceof CourseEntity) {
516
            return;
517
        }
518
519
        $lpRepo = Container::getLpRepository();
520
        $qb = $lpRepo->getResourcesByCourse($courseEntity, $sessionEntity);
521
522
        if (!empty($idList)) {
523
            $qb->andWhere('resource.iid IN (:ids)')
524
                ->setParameter('ids', array_values(array_unique(array_map('intval', $idList))))
525
            ;
526
        }
527
528
        /** @var CLp[] $lps */
529
        $lps = $qb->getQuery()->getResult();
530
531
        foreach ($lps as $lp) {
532
            $iid = (int) $lp->getIid();
533
            $lpType = (int) $lp->getLpType(); // 1=LP, 2=SCORM, 3=AICC
534
535
            $items = [];
536
537
            /** @var CLpItem $it */
538
            foreach ($lp->getItems() as $it) {
539
                $items[] = [
540
                    'id' => (int) $it->getIid(),
541
                    'item_type' => (string) $it->getItemType(),
542
                    'ref' => (string) $it->getRef(),
543
                    'title' => (string) $it->getTitle(),
544
                    'name' => (string) $lp->getTitle(),
545
                    'description' => (string) ($it->getDescription() ?? ''),
546
                    'path' => (string) $it->getPath(),
547
                    'min_score' => (float) $it->getMinScore(),
548
                    'max_score' => null !== $it->getMaxScore() ? (float) $it->getMaxScore() : null,
549
                    'mastery_score' => null !== $it->getMasteryScore() ? (float) $it->getMasteryScore() : null,
550
                    'parent_item_id' => (int) $it->getParentItemId(),
551
                    'previous_item_id' => null !== $it->getPreviousItemId() ? (int) $it->getPreviousItemId() : null,
552
                    'next_item_id' => null !== $it->getNextItemId() ? (int) $it->getNextItemId() : null,
553
                    'display_order' => (int) $it->getDisplayOrder(),
554
                    'prerequisite' => (string) ($it->getPrerequisite() ?? ''),
555
                    'parameters' => (string) ($it->getParameters() ?? ''),
556
                    'launch_data' => (string) $it->getLaunchData(),
557
                    'audio' => (string) ($it->getAudio() ?? ''),
558
                ];
559
            }
560
561
            $payload = [
562
                'id' => $iid,
563
                'lp_type' => $lpType,
564
                'title' => (string) $lp->getTitle(),
565
                'path' => (string) $lp->getPath(),
566
                'ref' => (string) ($lp->getRef() ?? ''),
567
                'description' => (string) ($lp->getDescription() ?? ''),
568
                'content_local' => (string) $lp->getContentLocal(),
569
                'default_encoding' => (string) $lp->getDefaultEncoding(),
570
                'default_view_mod' => (string) $lp->getDefaultViewMod(),
571
                'prevent_reinit' => (bool) $lp->getPreventReinit(),
572
                'force_commit' => (bool) $lp->getForceCommit(),
573
                'content_maker' => (string) $lp->getContentMaker(),
574
                'display_order' => (int) $lp->getDisplayNotAllowedLp(),
575
                'js_lib' => (string) $lp->getJsLib(),
576
                'content_license' => (string) $lp->getContentLicense(),
577
                'debug' => (bool) $lp->getDebug(),
578
                'visibility' => '1',
579
                'author' => (string) $lp->getAuthor(),
580
                'use_max_score' => (int) $lp->getUseMaxScore(),
581
                'autolaunch' => (int) $lp->getAutolaunch(),
582
                'created_on' => $this->fmtDate($lp->getCreatedOn()),
583
                'modified_on' => $this->fmtDate($lp->getModifiedOn()),
584
                'published_on' => $this->fmtDate($lp->getPublishedOn()),
585
                'expired_on' => $this->fmtDate($lp->getExpiredOn()),
586
                'session_id' => (int) ($sessionEntity?->getId() ?? 0),
587
                'category_id' => (int) ($lp->getCategory()?->getIid() ?? 0),
588
                'items' => $items,
589
            ];
590
591
            $legacyCourse->resources[RESOURCE_LEARNPATH][$iid] =
592
                $this->mkLegacyItem(RESOURCE_LEARNPATH, $iid, $payload, ['items']);
593
        }
594
595
        // Optional: pack “scorm” folder (legacy parity)
596
        if ($addScormFolder && isset($this->course->backup_path)) {
597
            $scormDir = rtrim((string) $this->course->backup_path, '/').'/scorm';
598
            if (is_dir($scormDir) && ($dh = @opendir($scormDir))) {
599
                $i = 1;
600
                while (false !== ($file = readdir($dh))) {
601
                    if ('.' === $file || '..' === $file) {
602
                        continue;
603
                    }
604
                    if (is_dir($scormDir.'/'.$file)) {
605
                        $payload = ['path' => '/'.$file, 'name' => (string) $file];
606
                        $legacyCourse->resources['scorm'][$i] =
607
                            $this->mkLegacyItem('scorm', $i, $payload);
608
                        $i++;
609
                    }
610
                }
611
                closedir($dh);
612
            }
613
        }
614
    }
615
616
    /**
617
     * Export Gradebook (categories + evaluations + links).
618
     */
619
    private function build_gradebook(
620
        object $legacyCourse,
621
        ?CourseEntity $courseEntity,
622
        ?SessionEntity $sessionEntity
623
    ): void {
624
        if (!$courseEntity instanceof CourseEntity) {
625
            return;
626
        }
627
628
        /** @var EntityManagerInterface $em */
629
        $em = Database::getManager();
630
        $catRepo = $em->getRepository(GradebookCategory::class);
631
632
        $criteria = ['course' => $courseEntity];
633
        if ($sessionEntity) {
634
            $criteria['session'] = $sessionEntity;
635
        }
636
637
        /** @var GradebookCategory[] $cats */
638
        $cats = $catRepo->findBy($criteria);
639
        if (!$cats) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cats of type Chamilo\CoreBundle\Entity\GradebookCategory[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
640
            return;
641
        }
642
643
        $payloadCategories = [];
644
        foreach ($cats as $cat) {
645
            $payloadCategories[] = $this->serializeGradebookCategory($cat);
646
        }
647
648
        $backup = new GradeBookBackup($payloadCategories);
649
        $legacyCourse->add_resource($backup);
650
    }
651
652
    /**
653
     * Serialize GradebookCategory (and nested parts) to array for restore.
654
     *
655
     * @return array<string,mixed>
656
     */
657
    private function serializeGradebookCategory(GradebookCategory $c): array
658
    {
659
        $arr = [
660
            'id' => (int) $c->getId(),
661
            'title' => (string) $c->getTitle(),
662
            'description' => (string) ($c->getDescription() ?? ''),
663
            'weight' => (float) $c->getWeight(),
664
            'visible' => (bool) $c->getVisible(),
665
            'locked' => (int) $c->getLocked(),
666
            'parent_id' => $c->getParent() ? (int) $c->getParent()->getId() : 0,
667
            'generate_certificates' => (bool) $c->getGenerateCertificates(),
668
            'certificate_validity_period' => $c->getCertificateValidityPeriod(),
669
            'is_requirement' => (bool) $c->getIsRequirement(),
670
            'default_lowest_eval_exclude' => (bool) $c->getDefaultLowestEvalExclude(),
671
            'minimum_to_validate' => $c->getMinimumToValidate(),
672
            'gradebooks_to_validate_in_dependence' => $c->getGradeBooksToValidateInDependence(),
673
            'allow_skills_by_subcategory' => $c->getAllowSkillsBySubcategory(),
674
            // camelCase duplicates (future-proof)
675
            'generateCertificates' => (bool) $c->getGenerateCertificates(),
676
            'certificateValidityPeriod' => $c->getCertificateValidityPeriod(),
677
            'isRequirement' => (bool) $c->getIsRequirement(),
678
            'defaultLowestEvalExclude' => (bool) $c->getDefaultLowestEvalExclude(),
679
            'minimumToValidate' => $c->getMinimumToValidate(),
680
            'gradeBooksToValidateInDependence' => $c->getGradeBooksToValidateInDependence(),
681
            'allowSkillsBySubcategory' => $c->getAllowSkillsBySubcategory(),
682
        ];
683
684
        if ($c->getGradeModel()) {
685
            $arr['grade_model_id'] = (int) $c->getGradeModel()->getId();
686
        }
687
688
        // Evaluations
689
        $arr['evaluations'] = [];
690
        foreach ($c->getEvaluations() as $e) {
691
            /** @var GradebookEvaluation $e */
692
            $arr['evaluations'][] = [
693
                'title' => (string) $e->getTitle(),
694
                'description' => (string) ($e->getDescription() ?? ''),
695
                'weight' => (float) $e->getWeight(),
696
                'max' => (float) $e->getMax(),
697
                'type' => (string) $e->getType(),
698
                'visible' => (int) $e->getVisible(),
699
                'locked' => (int) $e->getLocked(),
700
                'best_score' => $e->getBestScore(),
701
                'average_score' => $e->getAverageScore(),
702
                'score_weight' => $e->getScoreWeight(),
703
                'min_score' => $e->getMinScore(),
704
            ];
705
        }
706
707
        // Links
708
        $arr['links'] = [];
709
        foreach ($c->getLinks() as $l) {
710
            /** @var GradebookLink $l */
711
            $arr['links'][] = [
712
                'type' => (int) $l->getType(),
713
                'ref_id' => (int) $l->getRefId(),
714
                'weight' => (float) $l->getWeight(),
715
                'visible' => (int) $l->getVisible(),
716
                'locked' => (int) $l->getLocked(),
717
                'best_score' => $l->getBestScore(),
718
                'average_score' => $l->getAverageScore(),
719
                'score_weight' => $l->getScoreWeight(),
720
                'min_score' => $l->getMinScore(),
721
            ];
722
        }
723
724
        return $arr;
725
    }
726
727
    /**
728
     * Export Works (root folders only; include assignment params).
729
     *
730
     * @param array<int> $ids
731
     */
732
    private function build_works(
733
        object $legacyCourse,
734
        ?CourseEntity $courseEntity,
735
        ?SessionEntity $sessionEntity,
736
        array $ids
737
    ): void {
738
        if (!$courseEntity instanceof CourseEntity) {
739
            return;
740
        }
741
742
        $repo = Container::getStudentPublicationRepository();
743
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
744
745
        $qb
746
            ->andWhere('resource.publicationParent IS NULL')
747
            ->andWhere('resource.filetype = :ft')->setParameter('ft', 'folder')
748
            ->andWhere('resource.active = 1')
749
        ;
750
751
        if (!empty($ids)) {
752
            $qb->andWhere('resource.iid IN (:ids)')
753
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
754
            ;
755
        }
756
757
        /** @var CStudentPublication[] $rows */
758
        $rows = $qb->getQuery()->getResult();
759
760
        foreach ($rows as $row) {
761
            $iid = (int) $row->getIid();
762
            $title = (string) $row->getTitle();
763
            $desc = (string) ($row->getDescription() ?? '');
764
765
            // Detect documents linked in description
766
            $this->findAndSetDocumentsInText($desc);
767
768
            $asgmt = $row->getAssignment();
769
            $expiresOn = $asgmt?->getExpiresOn()?->format('Y-m-d H:i:s');
770
            $endsOn = $asgmt?->getEndsOn()?->format('Y-m-d H:i:s');
771
            $addToCal = $asgmt && $asgmt->getEventCalendarId() > 0 ? 1 : 0;
772
            $enableQ = (bool) ($asgmt?->getEnableQualification() ?? false);
773
774
            $params = [
775
                'id' => $iid,
776
                'title' => $title,
777
                'description' => $desc,
778
                'weight' => (float) $row->getWeight(),
779
                'qualification' => (float) $row->getQualification(),
780
                'allow_text_assignment' => (int) $row->getAllowTextAssignment(),
781
                'default_visibility' => (bool) ($row->getDefaultVisibility() ?? false),
782
                'student_delete_own_publication' => (bool) ($row->getStudentDeleteOwnPublication() ?? false),
783
                'extensions' => $row->getExtensions(),
784
                'group_category_work_id' => (int) $row->getGroupCategoryWorkId(),
785
                'post_group_id' => (int) $row->getPostGroupId(),
786
                'enable_qualification' => $enableQ,
787
                'add_to_calendar' => $addToCal ? 1 : 0,
788
                'expires_on' => $expiresOn ?: null,
789
                'ends_on' => $endsOn ?: null,
790
                'name' => $title,
791
                'url' => null,
792
            ];
793
794
            $legacy = new Work($params);
795
            $legacyCourse->add_resource($legacy);
796
        }
797
    }
798
799
    /**
800
     * Export Attendance + calendars.
801
     *
802
     * @param array<int> $ids
803
     */
804
    private function build_attendance(
805
        object $legacyCourse,
806
        ?CourseEntity $courseEntity,
807
        ?SessionEntity $sessionEntity,
808
        array $ids
809
    ): void {
810
        if (!$courseEntity instanceof CourseEntity) {
811
            return;
812
        }
813
814
        $repo = Container::getAttendanceRepository();
815
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
816
817
        if (!empty($ids)) {
818
            $qb->andWhere('resource.iid IN (:ids)')
819
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
820
            ;
821
        }
822
823
        /** @var CAttendance[] $rows */
824
        $rows = $qb->getQuery()->getResult();
825
826
        foreach ($rows as $row) {
827
            $iid = (int) $row->getIid();
828
            $title = (string) $row->getTitle();
829
            $desc = (string) ($row->getDescription() ?? '');
830
            $active = (int) $row->getActive();
831
832
            $this->findAndSetDocumentsInText($desc);
833
834
            $params = [
835
                'id' => $iid,
836
                'title' => $title,
837
                'description' => $desc,
838
                'active' => $active,
839
                'attendance_qualify_title' => (string) ($row->getAttendanceQualifyTitle() ?? ''),
840
                'attendance_qualify_max' => (int) $row->getAttendanceQualifyMax(),
841
                'attendance_weight' => (float) $row->getAttendanceWeight(),
842
                'locked' => (int) $row->getLocked(),
843
                'name' => $title,
844
            ];
845
846
            $legacy = new Attendance($params);
847
848
            /** @var CAttendanceCalendar $cal */
849
            foreach ($row->getCalendars() as $cal) {
850
                $calArr = [
851
                    'id' => (int) $cal->getIid(),
852
                    'attendance_id' => $iid,
853
                    'date_time' => $cal->getDateTime()?->format('Y-m-d H:i:s') ?? '',
854
                    'done_attendance' => (bool) $cal->getDoneAttendance(),
855
                    'blocked' => (bool) $cal->getBlocked(),
856
                    'duration' => null !== $cal->getDuration() ? (int) $cal->getDuration() : null,
857
                ];
858
                $legacy->add_attendance_calendar($calArr);
859
            }
860
861
            $legacyCourse->add_resource($legacy);
862
        }
863
    }
864
865
    /**
866
     * Export Thematic + advances + plans (and collect linked docs).
867
     *
868
     * @param array<int> $ids
869
     */
870
    private function build_thematic(
871
        object $legacyCourse,
872
        ?CourseEntity $courseEntity,
873
        ?SessionEntity $sessionEntity,
874
        array $ids
875
    ): void {
876
        if (!$courseEntity instanceof CourseEntity) {
877
            return;
878
        }
879
880
        $repo = Container::getThematicRepository();
881
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
882
883
        if (!empty($ids)) {
884
            $qb->andWhere('resource.iid IN (:ids)')
885
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
886
            ;
887
        }
888
889
        /** @var CThematic[] $rows */
890
        $rows = $qb->getQuery()->getResult();
891
892
        foreach ($rows as $row) {
893
            $iid = (int) $row->getIid();
894
            $title = (string) $row->getTitle();
895
            $content = (string) ($row->getContent() ?? '');
896
            $active = (bool) $row->getActive();
897
898
            $this->findAndSetDocumentsInText($content);
899
900
            $params = [
901
                'id' => $iid,
902
                'title' => $title,
903
                'content' => $content,
904
                'active' => $active,
905
            ];
906
907
            $legacy = new Thematic($params);
908
909
            /** @var CThematicAdvance $adv */
910
            foreach ($row->getAdvances() as $adv) {
911
                $attendanceId = 0;
912
913
                try {
914
                    $refAtt = new ReflectionProperty(CThematicAdvance::class, 'attendance');
915
                    if ($refAtt->isInitialized($adv)) {
916
                        $att = $adv->getAttendance();
917
                        if ($att) {
918
                            $attendanceId = (int) $att->getIid();
919
                        }
920
                    }
921
                } catch (Throwable) {
922
                    // keep $attendanceId = 0
923
                }
924
925
                $advArr = [
926
                    'id' => (int) $adv->getIid(),
927
                    'thematic_id' => (int) $row->getIid(),
928
                    'content' => (string) ($adv->getContent() ?? ''),
929
                    'start_date' => $adv->getStartDate()?->format('Y-m-d H:i:s') ?? '',
930
                    'duration' => (int) $adv->getDuration(),
931
                    'done_advance' => (bool) $adv->getDoneAdvance(),
932
                    'attendance_id' => $attendanceId,
933
                    'room_id' => (int) ($adv->getRoom()?->getId() ?? 0),
934
                ];
935
936
                $this->findAndSetDocumentsInText((string) $advArr['content']);
937
                $legacy->addThematicAdvance($advArr);
938
            }
939
940
            /** @var CThematicPlan $pl */
941
            foreach ($row->getPlans() as $pl) {
942
                $plArr = [
943
                    'id' => (int) $pl->getIid(),
944
                    'thematic_id' => $iid,
945
                    'title' => (string) $pl->getTitle(),
946
                    'description' => (string) ($pl->getDescription() ?? ''),
947
                    'description_type' => (int) $pl->getDescriptionType(),
948
                ];
949
                $this->findAndSetDocumentsInText((string) $plArr['description']);
950
                $legacy->addThematicPlan($plArr);
951
            }
952
953
            $legacyCourse->add_resource($legacy);
954
        }
955
    }
956
957
    /**
958
     * Export Wiki pages (content + metadata; collect docs in content).
959
     *
960
     * @param array<int> $ids
961
     */
962
    private function build_wiki(
963
        object $legacyCourse,
964
        ?CourseEntity $courseEntity,
965
        ?SessionEntity $sessionEntity,
966
        array $ids
967
    ): void {
968
        if (!$courseEntity instanceof CourseEntity) {
969
            return;
970
        }
971
972
        $repo = Container::getWikiRepository();
973
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
974
975
        if (!empty($ids)) {
976
            $qb->andWhere('resource.iid IN (:ids)')
977
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
978
            ;
979
        }
980
981
        /** @var CWiki[] $pages */
982
        $pages = $qb->getQuery()->getResult();
983
984
        foreach ($pages as $page) {
985
            $iid = (int) $page->getIid();
986
            $pageId = (int) ($page->getPageId() ?? $iid);
987
            $reflink = (string) $page->getReflink();
988
            $title = (string) $page->getTitle();
989
            $content = (string) $page->getContent();
990
            $userId = (int) $page->getUserId();
991
            $groupId = (int) ($page->getGroupId() ?? 0);
992
            $progress = (string) ($page->getProgress() ?? '');
993
            $version = (int) ($page->getVersion() ?? 1);
994
            $dtime = $page->getDtime()?->format('Y-m-d H:i:s') ?? '';
995
996
            $this->findAndSetDocumentsInText($content);
997
998
            $legacy = new Wiki(
999
                $iid,
1000
                $pageId,
1001
                $reflink,
1002
                $title,
1003
                $content,
1004
                $userId,
1005
                $groupId,
1006
                $dtime,
1007
                $progress,
1008
                $version
1009
            );
1010
1011
            $this->course->add_resource($legacy);
1012
        }
1013
    }
1014
1015
    /**
1016
     * Export Glossary terms (collect docs in descriptions).
1017
     *
1018
     * @param array<int> $ids
1019
     */
1020
    private function build_glossary(
1021
        object $legacyCourse,
1022
        ?CourseEntity $courseEntity,
1023
        ?SessionEntity $sessionEntity,
1024
        array $ids
1025
    ): void {
1026
        if (!$courseEntity instanceof CourseEntity) {
1027
            return;
1028
        }
1029
1030
        $repo = Container::getGlossaryRepository();
1031
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
1032
1033
        if (!empty($ids)) {
1034
            $qb->andWhere('resource.iid IN (:ids)')
1035
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1036
            ;
1037
        }
1038
1039
        /** @var CGlossary[] $terms */
1040
        $terms = $qb->getQuery()->getResult();
1041
1042
        foreach ($terms as $term) {
1043
            $iid = (int) $term->getIid();
1044
            $title = (string) $term->getTitle();
1045
            $desc = (string) ($term->getDescription() ?? '');
1046
1047
            $this->findAndSetDocumentsInText($desc);
1048
1049
            $legacy = new Glossary(
1050
                $iid,
1051
                $title,
1052
                $desc,
1053
                0
1054
            );
1055
1056
            $this->course->add_resource($legacy);
1057
        }
1058
    }
1059
1060
    /**
1061
     * Export Course descriptions (collect docs in HTML).
1062
     *
1063
     * @param array<int> $ids
1064
     */
1065
    private function build_course_descriptions(
1066
        object $legacyCourse,
1067
        ?CourseEntity $courseEntity,
1068
        ?SessionEntity $sessionEntity,
1069
        array $ids
1070
    ): void {
1071
        if (!$courseEntity instanceof CourseEntity) {
1072
            return;
1073
        }
1074
1075
        $repo = Container::getCourseDescriptionRepository();
1076
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
1077
1078
        if (!empty($ids)) {
1079
            $qb->andWhere('resource.iid IN (:ids)')
1080
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1081
            ;
1082
        }
1083
1084
        /** @var CCourseDescription[] $rows */
1085
        $rows = $qb->getQuery()->getResult();
1086
1087
        foreach ($rows as $row) {
1088
            $iid = (int) $row->getIid();
1089
            $title = (string) ($row->getTitle() ?? '');
1090
            $html = (string) ($row->getContent() ?? '');
1091
            $type = (int) $row->getDescriptionType();
1092
1093
            $this->findAndSetDocumentsInText($html);
1094
1095
            $export = new CourseDescription(
1096
                $iid,
1097
                $title,
1098
                $html,
1099
                $type
1100
            );
1101
1102
            $this->course->add_resource($export);
1103
        }
1104
    }
1105
1106
    /**
1107
     * Export Calendar events (first attachment as legacy, all as assets).
1108
     *
1109
     * @param array<int> $ids
1110
     */
1111
    private function build_events(
1112
        object $legacyCourse,
1113
        ?CourseEntity $courseEntity,
1114
        ?SessionEntity $sessionEntity,
1115
        array $ids
1116
    ): void {
1117
        if (!$courseEntity instanceof CourseEntity) {
1118
            return;
1119
        }
1120
1121
        $eventRepo = Container::getCalendarEventRepository();
1122
        $qb = $eventRepo->getResourcesByCourse($courseEntity, $sessionEntity);
1123
1124
        if (!empty($ids)) {
1125
            $qb->andWhere('resource.iid IN (:ids)')
1126
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1127
            ;
1128
        }
1129
1130
        /** @var CCalendarEvent[] $events */
1131
        $events = $qb->getQuery()->getResult();
1132
1133
        /** @var KernelInterface $kernel */
1134
        $kernel = Container::$container->get('kernel');
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1134
        /** @scrutinizer ignore-call */ 
1135
        $kernel = Container::$container->get('kernel');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1135
        $projectDir = rtrim($kernel->getProjectDir(), '/');
1136
        $resourceBase = $projectDir.'/var/upload/resource';
1137
1138
        /** @var ResourceNodeRepository $rnRepo */
1139
        $rnRepo = Container::$container->get(ResourceNodeRepository::class);
1140
1141
        foreach ($events as $ev) {
1142
            $iid = (int) $ev->getIid();
1143
            $title = (string) $ev->getTitle();
1144
            $content = (string) ($ev->getContent() ?? '');
1145
            $startDate = $ev->getStartDate()?->format('Y-m-d H:i:s') ?? '';
1146
            $endDate = $ev->getEndDate()?->format('Y-m-d H:i:s') ?? '';
1147
            $allDay = (int) $ev->isAllDay();
1148
1149
            $firstPath = $firstName = $firstComment = '';
1150
            $firstSize = 0;
1151
1152
            /** @var CCalendarEventAttachment $att */
1153
            foreach ($ev->getAttachments() as $att) {
1154
                $node = $att->getResourceNode();
1155
                $abs = null;
1156
                $size = 0;
1157
                $relForZip = null;
1158
1159
                if ($node) {
1160
                    $file = $node->getFirstResourceFile();
1161
                    if ($file) {
1162
                        $storedRel = (string) $rnRepo->getFilename($file);
1163
                        if ('' !== $storedRel) {
1164
                            $candidate = $resourceBase.$storedRel;
1165
                            if (is_readable($candidate)) {
1166
                                $abs = $candidate;
1167
                                $size = (int) $file->getSize();
1168
                                if ($size <= 0 && is_file($candidate)) {
1169
                                    $st = @stat($candidate);
1170
                                    $size = $st ? (int) $st['size'] : 0;
1171
                                }
1172
                                $base = basename($storedRel) ?: (string) $att->getIid();
1173
                                $relForZip = 'upload/calendar/'.$base;
1174
                            }
1175
                        }
1176
                    }
1177
                }
1178
1179
                if ($abs && $relForZip) {
1180
                    $this->tryAddAsset($relForZip, $abs, $size);
1181
                } else {
1182
                    error_log('COURSE_BUILD: event attachment file not found (event_iid='
1183
                        .$iid.'; att_iid='.(int) $att->getIid().')');
1184
                }
1185
1186
                if ('' === $firstName && $relForZip) {
1187
                    $firstPath = substr($relForZip, \strlen('upload/calendar/'));
1188
                    $firstName = (string) $att->getFilename();
1189
                    $firstComment = (string) ($att->getComment() ?? '');
1190
                    $firstSize = (int) $size;
1191
                }
1192
            }
1193
1194
            $export = new CalendarEvent(
1195
                $iid,
1196
                $title,
1197
                $content,
1198
                $startDate,
1199
                $endDate,
1200
                $firstPath,
1201
                $firstName,
1202
                $firstSize,
1203
                $firstComment,
1204
                $allDay
1205
            );
1206
1207
            $this->course->add_resource($export);
1208
        }
1209
    }
1210
1211
    /**
1212
     * Export Announcements (first attachment legacy, all as assets).
1213
     *
1214
     * @param array<int> $ids
1215
     */
1216
    private function build_announcements(
1217
        object $legacyCourse,
1218
        ?CourseEntity $courseEntity,
1219
        ?SessionEntity $sessionEntity,
1220
        array $ids
1221
    ): void {
1222
        if (!$courseEntity) {
1223
            return;
1224
        }
1225
1226
        $annRepo = Container::getAnnouncementRepository();
1227
        $qb = $annRepo->getResourcesByCourse($courseEntity, $sessionEntity);
1228
1229
        if (!empty($ids)) {
1230
            $qb->andWhere('resource.iid IN (:ids)')
1231
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1232
            ;
1233
        }
1234
1235
        /** @var CAnnouncement[] $anns */
1236
        $anns = $qb->getQuery()->getResult();
1237
1238
        /** @var KernelInterface $kernel */
1239
        $kernel = Container::$container->get('kernel');
1240
        $projectDir = rtrim($kernel->getProjectDir(), '/');
1241
        $resourceBase = $projectDir.'/var/upload/resource';
1242
1243
        /** @var ResourceNodeRepository $rnRepo */
1244
        $rnRepo = Container::$container->get(ResourceNodeRepository::class);
1245
1246
        foreach ($anns as $a) {
1247
            $iid = (int) $a->getIid();
1248
            $title = (string) $a->getTitle();
1249
            $html = (string) ($a->getContent() ?? '');
1250
            $date = $a->getEndDate()?->format('Y-m-d H:i:s') ?? '';
1251
            $email = (bool) $a->getEmailSent();
1252
1253
            $firstPath = $firstName = $firstComment = '';
1254
            $firstSize = 0;
1255
1256
            $attachmentsArr = [];
1257
1258
            /** @var CAnnouncementAttachment $att */
1259
            foreach ($a->getAttachments() as $att) {
1260
                $relPath = ltrim((string) $att->getPath(), '/');
1261
                $assetRel = 'upload/announcements/'.$relPath;
1262
1263
                $abs = null;
1264
                $node = $att->getResourceNode();
1265
                if ($node) {
1266
                    $file = $node->getFirstResourceFile();
1267
                    if ($file) {
1268
                        $storedRel = (string) $rnRepo->getFilename($file);
1269
                        if ('' !== $storedRel) {
1270
                            $candidate = $resourceBase.$storedRel;
1271
                            if (is_readable($candidate)) {
1272
                                $abs = $candidate;
1273
                            }
1274
                        }
1275
                    }
1276
                }
1277
1278
                if ($abs) {
1279
                    $this->tryAddAsset($assetRel, $abs, (int) $att->getSize());
1280
                } else {
1281
                    error_log('COURSE_BUILD: announcement attachment not found (iid='.(int) $att->getIid().')');
1282
                }
1283
1284
                $attachmentsArr[] = [
1285
                    'path' => $relPath,
1286
                    'filename' => (string) $att->getFilename(),
1287
                    'size' => (int) $att->getSize(),
1288
                    'comment' => (string) ($att->getComment() ?? ''),
1289
                    'asset_relpath' => $assetRel,
1290
                ];
1291
1292
                if ('' === $firstName) {
1293
                    $firstPath = $relPath;
1294
                    $firstName = (string) $att->getFilename();
1295
                    $firstSize = (int) $att->getSize();
1296
                    $firstComment = (string) ($att->getComment() ?? '');
1297
                }
1298
            }
1299
1300
            $payload = [
1301
                'title' => $title,
1302
                'content' => $html,
1303
                'date' => $date,
1304
                'display_order' => 0,
1305
                'email_sent' => $email ? 1 : 0,
1306
                'attachment_path' => $firstPath,
1307
                'attachment_filename' => $firstName,
1308
                'attachment_size' => $firstSize,
1309
                'attachment_comment' => $firstComment,
1310
                'attachments' => $attachmentsArr,
1311
            ];
1312
1313
            $legacyCourse->resources[RESOURCE_ANNOUNCEMENT][$iid] =
1314
                $this->mkLegacyItem(RESOURCE_ANNOUNCEMENT, $iid, $payload, ['attachments']);
1315
        }
1316
    }
1317
1318
    /**
1319
     * Register an asset to be packed into the export ZIP.
1320
     *
1321
     * @param string $relPath Relative path inside the ZIP
1322
     * @param string $absPath Absolute filesystem path
1323
     */
1324
    private function addAsset(string $relPath, string $absPath, int $size = 0): void
1325
    {
1326
        if (!isset($this->course->resources['asset']) || !\is_array($this->course->resources['asset'])) {
1327
            $this->course->resources['asset'] = [];
1328
        }
1329
        $this->course->resources['asset'][$relPath] = [
1330
            'abs' => $absPath,
1331
            'size' => $size,
1332
        ];
1333
    }
1334
1335
    /**
1336
     * Try to add an asset only if file exists.
1337
     */
1338
    private function tryAddAsset(string $relPath, string $absPath, int $size = 0): void
1339
    {
1340
        if (is_file($absPath) && is_readable($absPath)) {
1341
            $this->addAsset($relPath, $absPath, $size);
1342
        } else {
1343
            error_log('COURSE_BUILD: asset missing: '.$absPath);
1344
        }
1345
    }
1346
1347
    /**
1348
     * Export Surveys; returns needed Question IDs for follow-up export.
1349
     *
1350
     * @param array<int> $surveyIds
1351
     *
1352
     * @return array<int>
1353
     */
1354
    private function build_surveys(
1355
        object $legacyCourse,
1356
        ?CourseEntity $courseEntity,
1357
        ?SessionEntity $sessionEntity,
1358
        array $surveyIds
1359
    ): array {
1360
        if (!$courseEntity) {
1361
            return [];
1362
        }
1363
1364
        $qb = $this->em->createQueryBuilder()
1365
            ->select('s')
1366
            ->from(CSurvey::class, 's')
1367
            ->innerJoin('s.resourceNode', 'rn')
1368
            ->leftJoin('rn.resourceLinks', 'links')
1369
            ->andWhere('links.course = :course')->setParameter('course', $courseEntity)
1370
            ->andWhere(
1371
                $sessionEntity
1372
                ? '(links.session IS NULL OR links.session = :session)'
1373
                : 'links.session IS NULL'
1374
            )
1375
            ->andWhere('links.deletedAt IS NULL')
1376
            ->andWhere('links.endVisibilityAt IS NULL')
1377
        ;
1378
1379
        if ($sessionEntity) {
1380
            $qb->setParameter('session', $sessionEntity);
1381
        }
1382
        if (!empty($surveyIds)) {
1383
            $qb->andWhere('s.iid IN (:ids)')->setParameter('ids', array_map('intval', $surveyIds));
1384
        }
1385
1386
        /** @var CSurvey[] $surveys */
1387
        $surveys = $qb->getQuery()->getResult();
1388
1389
        $neededQuestionIds = [];
1390
1391
        foreach ($surveys as $s) {
1392
            $iid = (int) $s->getIid();
1393
            $qIds = [];
1394
1395
            foreach ($s->getQuestions() as $q) {
1396
                /** @var CSurveyQuestion $q */
1397
                $qid = (int) $q->getIid();
1398
                $qIds[] = $qid;
1399
                $neededQuestionIds[$qid] = true;
1400
            }
1401
1402
            $payload = [
1403
                'code' => (string) ($s->getCode() ?? ''),
1404
                'title' => (string) $s->getTitle(),
1405
                'subtitle' => (string) ($s->getSubtitle() ?? ''),
1406
                'author' => '',
1407
                'lang' => (string) ($s->getLang() ?? ''),
1408
                'avail_from' => $s->getAvailFrom()?->format('Y-m-d H:i:s'),
1409
                'avail_till' => $s->getAvailTill()?->format('Y-m-d H:i:s'),
1410
                'is_shared' => (string) ($s->getIsShared() ?? '0'),
1411
                'template' => (string) ($s->getTemplate() ?? 'template'),
1412
                'intro' => (string) ($s->getIntro() ?? ''),
1413
                'surveythanks' => (string) ($s->getSurveythanks() ?? ''),
1414
                'creation_date' => $s->getCreationDate()?->format('Y-m-d H:i:s') ?: date('Y-m-d H:i:s'),
1415
                'invited' => (int) $s->getInvited(),
1416
                'answered' => (int) $s->getAnswered(),
1417
                'invite_mail' => (string) $s->getInviteMail(),
1418
                'reminder_mail' => (string) $s->getReminderMail(),
1419
                'mail_subject' => (string) $s->getMailSubject(),
1420
                'anonymous' => (string) $s->getAnonymous(),
1421
                'shuffle' => (bool) $s->getShuffle(),
1422
                'one_question_per_page' => (bool) $s->getOneQuestionPerPage(),
1423
                'visible_results' => $s->getVisibleResults(),
1424
                'display_question_number' => (bool) $s->isDisplayQuestionNumber(),
1425
                'survey_type' => (int) $s->getSurveyType(),
1426
                'show_form_profile' => (int) $s->getShowFormProfile(),
1427
                'form_fields' => (string) $s->getFormFields(),
1428
                'duration' => $s->getDuration(),
1429
                'question_ids' => $qIds,
1430
                'survey_id' => $iid,
1431
            ];
1432
1433
            $legacyCourse->resources[RESOURCE_SURVEY][$iid] =
1434
                $this->mkLegacyItem(RESOURCE_SURVEY, $iid, $payload);
1435
1436
            error_log('COURSE_BUILD: SURVEY iid='.$iid.' qids=['.implode(',', $qIds).']');
1437
        }
1438
1439
        return array_keys($neededQuestionIds);
1440
    }
1441
1442
    /**
1443
     * Export Survey Questions (answers promoted at top level).
1444
     *
1445
     * @param array<int> $questionIds
1446
     */
1447
    private function build_survey_questions(
1448
        object $legacyCourse,
1449
        ?CourseEntity $courseEntity,
1450
        ?SessionEntity $sessionEntity,
1451
        array $questionIds
1452
    ): void {
1453
        if (!$courseEntity) {
1454
            return;
1455
        }
1456
1457
        $qb = $this->em->createQueryBuilder()
1458
            ->select('q', 's')
1459
            ->from(CSurveyQuestion::class, 'q')
1460
            ->innerJoin('q.survey', 's')
1461
            ->innerJoin('s.resourceNode', 'rn')
1462
            ->leftJoin('rn.resourceLinks', 'links')
1463
            ->andWhere('links.course = :course')->setParameter('course', $courseEntity)
1464
            ->andWhere(
1465
                $sessionEntity
1466
                ? '(links.session IS NULL OR links.session = :session)'
1467
                : 'links.session IS NULL'
1468
            )
1469
            ->andWhere('links.deletedAt IS NULL')
1470
            ->andWhere('links.endVisibilityAt IS NULL')
1471
            ->orderBy('s.iid', 'ASC')
1472
            ->addOrderBy('q.sort', 'ASC')
1473
        ;
1474
1475
        if ($sessionEntity) {
1476
            $qb->setParameter('session', $sessionEntity);
1477
        }
1478
        if (!empty($questionIds)) {
1479
            $qb->andWhere('q.iid IN (:ids)')->setParameter('ids', array_map('intval', $questionIds));
1480
        }
1481
1482
        /** @var CSurveyQuestion[] $questions */
1483
        $questions = $qb->getQuery()->getResult();
1484
1485
        $exported = 0;
1486
1487
        foreach ($questions as $q) {
1488
            $qid = (int) $q->getIid();
1489
            $sid = (int) $q->getSurvey()->getIid();
1490
1491
            $answers = [];
1492
            foreach ($q->getOptions() as $opt) {
1493
                /** @var CSurveyQuestionOption $opt */
1494
                $answers[] = [
1495
                    'option_text' => (string) $opt->getOptionText(),
1496
                    'sort' => (int) $opt->getSort(),
1497
                    'value' => (int) $opt->getValue(),
1498
                ];
1499
            }
1500
1501
            $payload = [
1502
                'survey_id' => $sid,
1503
                'survey_question' => (string) $q->getSurveyQuestion(),
1504
                'survey_question_comment' => (string) ($q->getSurveyQuestionComment() ?? ''),
1505
                'type' => (string) $q->getType(),
1506
                'display' => (string) $q->getDisplay(),
1507
                'sort' => (int) $q->getSort(),
1508
                'shared_question_id' => $q->getSharedQuestionId(),
1509
                'max_value' => $q->getMaxValue(),
1510
                'is_required' => (bool) $q->isMandatory(),
1511
                'answers' => $answers,
1512
            ];
1513
1514
            $legacyCourse->resources[RESOURCE_SURVEYQUESTION][$qid] =
1515
                $this->mkLegacyItem(RESOURCE_SURVEYQUESTION, $qid, $payload, ['answers']);
1516
1517
            $exported++;
1518
            error_log('COURSE_BUILD: SURVEY_Q qid='.$qid.' survey='.$sid.' answers='.\count($answers));
1519
        }
1520
1521
        error_log('COURSE_BUILD: survey questions exported='.$exported);
1522
    }
1523
1524
    /**
1525
     * Export Quizzes and return required Question IDs.
1526
     *
1527
     * @param array<int> $quizIds
1528
     *
1529
     * @return array<int>
1530
     */
1531
    private function build_quizzes(
1532
        object $legacyCourse,
1533
        ?CourseEntity $courseEntity,
1534
        ?SessionEntity $sessionEntity,
1535
        array $quizIds
1536
    ): array {
1537
        if (!$courseEntity) {
1538
            return [];
1539
        }
1540
1541
        $qb = $this->em->createQueryBuilder()
1542
            ->select('q')
1543
            ->from(CQuiz::class, 'q')
1544
            ->innerJoin('q.resourceNode', 'rn')
1545
            ->leftJoin('rn.resourceLinks', 'links')
1546
            ->andWhere('links.course = :course')->setParameter('course', $courseEntity)
1547
            ->andWhere(
1548
                $sessionEntity
1549
                ? '(links.session IS NULL OR links.session = :session)'
1550
                : 'links.session IS NULL'
1551
            )
1552
            ->andWhere('links.deletedAt IS NULL')
1553
            ->andWhere('links.endVisibilityAt IS NULL')
1554
        ;
1555
1556
        if ($sessionEntity) {
1557
            $qb->setParameter('session', $sessionEntity);
1558
        }
1559
        if (!empty($quizIds)) {
1560
            $qb->andWhere('q.iid IN (:ids)')->setParameter('ids', array_map('intval', $quizIds));
1561
        }
1562
1563
        /** @var CQuiz[] $quizzes */
1564
        $quizzes = $qb->getQuery()->getResult();
1565
        $neededQuestionIds = [];
1566
1567
        foreach ($quizzes as $quiz) {
1568
            $iid = (int) $quiz->getIid();
1569
1570
            $payload = [
1571
                'title' => (string) $quiz->getTitle(),
1572
                'description' => (string) ($quiz->getDescription() ?? ''),
1573
                'type' => (int) $quiz->getType(),
1574
                'random' => (int) $quiz->getRandom(),
1575
                'random_answers' => (bool) $quiz->getRandomAnswers(),
1576
                'results_disabled' => (int) $quiz->getResultsDisabled(),
1577
                'max_attempt' => (int) $quiz->getMaxAttempt(),
1578
                'feedback_type' => (int) $quiz->getFeedbackType(),
1579
                'expired_time' => (int) $quiz->getExpiredTime(),
1580
                'review_answers' => (int) $quiz->getReviewAnswers(),
1581
                'random_by_category' => (int) $quiz->getRandomByCategory(),
1582
                'text_when_finished' => (string) ($quiz->getTextWhenFinished() ?? ''),
1583
                'text_when_finished_failure' => (string) ($quiz->getTextWhenFinishedFailure() ?? ''),
1584
                'display_category_name' => (int) $quiz->getDisplayCategoryName(),
1585
                'save_correct_answers' => (int) ($quiz->getSaveCorrectAnswers() ?? 0),
1586
                'propagate_neg' => (int) $quiz->getPropagateNeg(),
1587
                'hide_question_title' => (bool) $quiz->isHideQuestionTitle(),
1588
                'hide_question_number' => (int) $quiz->getHideQuestionNumber(),
1589
                'question_selection_type' => (int) ($quiz->getQuestionSelectionType() ?? 0),
1590
                'access_condition' => (string) ($quiz->getAccessCondition() ?? ''),
1591
                'pass_percentage' => $quiz->getPassPercentage(),
1592
                'start_time' => $quiz->getStartTime()?->format('Y-m-d H:i:s'),
1593
                'end_time' => $quiz->getEndTime()?->format('Y-m-d H:i:s'),
1594
                'question_ids' => [],
1595
                'question_orders' => [],
1596
            ];
1597
1598
            $rels = $this->em->createQueryBuilder()
1599
                ->select('rel', 'qq')
1600
                ->from(CQuizRelQuestion::class, 'rel')
1601
                ->innerJoin('rel.question', 'qq')
1602
                ->andWhere('rel.quiz = :quiz')
1603
                ->setParameter('quiz', $quiz)
1604
                ->orderBy('rel.questionOrder', 'ASC')
1605
                ->getQuery()->getResult()
1606
            ;
1607
1608
            foreach ($rels as $rel) {
1609
                $qid = (int) $rel->getQuestion()->getIid();
1610
                $payload['question_ids'][] = $qid;
1611
                $payload['question_orders'][] = (int) $rel->getQuestionOrder();
1612
                $neededQuestionIds[$qid] = true;
1613
            }
1614
1615
            $legacyCourse->resources[RESOURCE_QUIZ][$iid] =
1616
                $this->mkLegacyItem(
1617
                    RESOURCE_QUIZ,
1618
                    $iid,
1619
                    $payload,
1620
                    ['question_ids', 'question_orders']
1621
                );
1622
        }
1623
1624
        error_log(
1625
            'COURSE_BUILD: build_quizzes done; total='.\count($quizzes)
1626
        );
1627
1628
        return array_keys($neededQuestionIds);
1629
    }
1630
1631
    /**
1632
     * Safe count helper for mixed values.
1633
     */
1634
    private function safeCount(mixed $v): int
1635
    {
1636
        return (\is_array($v) || $v instanceof Countable) ? \count($v) : 0;
1637
    }
1638
1639
    /**
1640
     * Export Quiz Questions (answers and options promoted).
1641
     *
1642
     * @param array<int> $questionIds
1643
     */
1644
    private function build_quiz_questions(
1645
        object $legacyCourse,
1646
        ?CourseEntity $courseEntity,
1647
        ?SessionEntity $sessionEntity,
1648
        array $questionIds
1649
    ): void {
1650
        if (!$courseEntity) {
1651
            return;
1652
        }
1653
1654
        error_log('COURSE_BUILD: build_quiz_questions start ids='.json_encode(array_values($questionIds)));
1655
        error_log('COURSE_BUILD: build_quiz_questions exported='.$this->safeCount($legacyCourse->resources[RESOURCE_QUIZQUESTION] ?? 0));
1656
1657
        $qb = $this->em->createQueryBuilder()
1658
            ->select('qq')
1659
            ->from(CQuizQuestion::class, 'qq')
1660
            ->innerJoin('qq.resourceNode', 'qrn')
1661
            ->leftJoin('qrn.resourceLinks', 'qlinks')
1662
            ->andWhere('qlinks.course = :course')->setParameter('course', $courseEntity)
1663
            ->andWhere(
1664
                $sessionEntity
1665
                ? '(qlinks.session IS NULL OR qlinks.session = :session)'
1666
                : 'qlinks.session IS NULL'
1667
            )
1668
            ->andWhere('qlinks.deletedAt IS NULL')
1669
            ->andWhere('qlinks.endVisibilityAt IS NULL')
1670
        ;
1671
1672
        if ($sessionEntity) {
1673
            $qb->setParameter('session', $sessionEntity);
1674
        }
1675
        if (!empty($questionIds)) {
1676
            $qb->andWhere('qq.iid IN (:ids)')->setParameter('ids', array_map('intval', $questionIds));
1677
        }
1678
1679
        /** @var CQuizQuestion[] $questions */
1680
        $questions = $qb->getQuery()->getResult();
1681
1682
        error_log('COURSE_BUILD: build_quiz_questions start ids='.json_encode(array_values($questionIds)));
1683
        error_log('COURSE_BUILD: build_quiz_questions exported='.$this->safeCount($legacyCourse->resources[RESOURCE_QUIZQUESTION] ?? 0));
1684
1685
        $this->exportQuestionsWithAnswers($legacyCourse, $questions);
1686
    }
1687
1688
    /**
1689
     * Internal exporter for quiz questions + answers (+ options for MATF type).
1690
     *
1691
     * @param array<int,CQuizQuestion> $questions
1692
     */
1693
    private function exportQuestionsWithAnswers(object $legacyCourse, array $questions): void
1694
    {
1695
        foreach ($questions as $q) {
1696
            $qid = (int) $q->getIid();
1697
1698
            $payload = [
1699
                'question' => (string) $q->getQuestion(),
1700
                'description' => (string) ($q->getDescription() ?? ''),
1701
                'ponderation' => (float) $q->getPonderation(),
1702
                'position' => (int) $q->getPosition(),
1703
                'type' => (int) $q->getType(),
1704
                'quiz_type' => (int) $q->getType(),
1705
                'picture' => (string) ($q->getPicture() ?? ''),
1706
                'level' => (int) $q->getLevel(),
1707
                'extra' => (string) ($q->getExtra() ?? ''),
1708
                'feedback' => (string) ($q->getFeedback() ?? ''),
1709
                'question_code' => (string) ($q->getQuestionCode() ?? ''),
1710
                'mandatory' => (int) $q->getMandatory(),
1711
                'duration' => $q->getDuration(),
1712
                'parent_media_id' => $q->getParentMediaId(),
1713
                'answers' => [],
1714
            ];
1715
1716
            $ans = $this->em->createQueryBuilder()
1717
                ->select('a')
1718
                ->from(CQuizAnswer::class, 'a')
1719
                ->andWhere('a.question = :q')->setParameter('q', $q)
1720
                ->orderBy('a.position', 'ASC')
1721
                ->getQuery()->getResult()
1722
            ;
1723
1724
            foreach ($ans as $a) {
1725
                $payload['answers'][] = [
1726
                    'id' => (int) $a->getIid(),
1727
                    'answer' => (string) $a->getAnswer(),
1728
                    'comment' => (string) ($a->getComment() ?? ''),
1729
                    'ponderation' => (float) $a->getPonderation(),
1730
                    'position' => (int) $a->getPosition(),
1731
                    'hotspot_coordinates' => $a->getHotspotCoordinates(),
1732
                    'hotspot_type' => $a->getHotspotType(),
1733
                    'correct' => $a->getCorrect(),
1734
                ];
1735
            }
1736
1737
            if (\defined('MULTIPLE_ANSWER_TRUE_FALSE') && MULTIPLE_ANSWER_TRUE_FALSE === (int) $q->getType()) {
1738
                $opts = $this->em->createQueryBuilder()
1739
                    ->select('o')
1740
                    ->from(CQuizQuestionOption::class, 'o')
1741
                    ->andWhere('o.question = :q')->setParameter('q', $q)
1742
                    ->orderBy('o.position', 'ASC')
1743
                    ->getQuery()->getResult()
1744
                ;
1745
1746
                $payload['question_options'] = array_map(static fn ($o) => [
1747
                    'id' => (int) $o->getIid(),
1748
                    'name' => (string) $o->getTitle(),
1749
                    'position' => (int) $o->getPosition(),
1750
                ], $opts);
1751
            }
1752
1753
            $legacyCourse->resources[RESOURCE_QUIZQUESTION][$qid] =
1754
                $this->mkLegacyItem(RESOURCE_QUIZQUESTION, $qid, $payload, ['answers', 'question_options']);
1755
1756
            error_log(
1757
                'COURSE_BUILD: QQ qid='.$qid.
1758
                ' quiz_type='.($legacyCourse->resources[RESOURCE_QUIZQUESTION][$qid]->quiz_type ?? 'missing').
1759
                ' answers='.\count($legacyCourse->resources[RESOURCE_QUIZQUESTION][$qid]->answers ?? [])
1760
            );
1761
        }
1762
    }
1763
1764
    /**
1765
     * Export Link category as legacy item.
1766
     */
1767
    private function build_link_category(CLinkCategory $category): void
1768
    {
1769
        $id = (int) $category->getIid();
1770
        if ($id <= 0) {
1771
            return;
1772
        }
1773
1774
        $payload = [
1775
            'title' => (string) $category->getTitle(),
1776
            'description' => (string) ($category->getDescription() ?? ''),
1777
            'category_title' => (string) $category->getTitle(),
1778
        ];
1779
1780
        $this->course->resources[RESOURCE_LINKCATEGORY][$id] =
1781
            $this->mkLegacyItem(RESOURCE_LINKCATEGORY, $id, $payload);
1782
    }
1783
1784
    /**
1785
     * Export Links (and their categories once).
1786
     *
1787
     * @param array<int> $ids
1788
     */
1789
    private function build_links(
1790
        object $legacyCourse,
1791
        ?CourseEntity $courseEntity,
1792
        ?SessionEntity $sessionEntity,
1793
        array $ids
1794
    ): void {
1795
        if (!$courseEntity instanceof CourseEntity) {
1796
            return;
1797
        }
1798
1799
        $linkRepo = Container::getLinkRepository();
1800
        $catRepo = Container::getLinkCategoryRepository();
1801
1802
        $qb = $linkRepo->getResourcesByCourse($courseEntity, $sessionEntity);
1803
1804
        if (!empty($ids)) {
1805
            $qb->andWhere('resource.iid IN (:ids)')
1806
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1807
            ;
1808
        }
1809
1810
        /** @var CLink[] $links */
1811
        $links = $qb->getQuery()->getResult();
1812
1813
        $exportedCats = [];
1814
1815
        foreach ($links as $link) {
1816
            $iid = (int) $link->getIid();
1817
            $title = (string) $link->getTitle();
1818
            $url = (string) $link->getUrl();
1819
            $desc = (string) ($link->getDescription() ?? '');
1820
            $tgt = (string) ($link->getTarget() ?? '');
1821
1822
            $cat = $link->getCategory();
1823
            $catId = (int) ($cat?->getIid() ?? 0);
1824
1825
            if ($catId > 0 && !isset($exportedCats[$catId])) {
1826
                $this->build_link_category($cat);
1827
                $exportedCats[$catId] = true;
1828
            }
1829
1830
            $payload = [
1831
                'title' => '' !== $title ? $title : $url,
1832
                'url' => $url,
1833
                'description' => $desc,
1834
                'target' => $tgt,
1835
                'category_id' => $catId,
1836
                'on_homepage' => false,
1837
            ];
1838
1839
            $legacyCourse->resources[RESOURCE_LINK][$iid] =
1840
                $this->mkLegacyItem(RESOURCE_LINK, $iid, $payload);
1841
        }
1842
    }
1843
1844
    /**
1845
     * Format DateTime as string "Y-m-d H:i:s".
1846
     */
1847
    private function fmtDate(?DateTimeInterface $dt): string
1848
    {
1849
        return $dt ? $dt->format('Y-m-d H:i:s') : '';
1850
    }
1851
1852
    /**
1853
     * Create a legacy item object, promoting selected array keys to top-level.
1854
     *
1855
     * @param array<int,string> $arrayKeysToPromote
1856
     */
1857
    private function mkLegacyItem(string $type, int $sourceId, array|object $obj, array $arrayKeysToPromote = []): stdClass
1858
    {
1859
        $o = new stdClass();
1860
        $o->type = $type;
1861
        $o->source_id = $sourceId;
1862
        $o->destination_id = -1;
1863
        $o->has_obj = true;
1864
        $o->obj = (object) $obj;
1865
1866
        if (!isset($o->obj->iid)) {
1867
            $o->obj->iid = $sourceId;
1868
        }
1869
        if (!isset($o->id)) {
1870
            $o->id = $sourceId;
1871
        }
1872
        if (!isset($o->obj->id)) {
1873
            $o->obj->id = $sourceId;
1874
        }
1875
1876
        foreach ((array) $obj as $k => $v) {
1877
            if (\is_scalar($v) || null === $v) {
1878
                if (!property_exists($o, $k)) {
1879
                    $o->{$k} = $v;
1880
                }
1881
            }
1882
        }
1883
        foreach ($arrayKeysToPromote as $k) {
1884
            if (isset($obj[$k]) && \is_array($obj[$k])) {
1885
                $o->{$k} = $obj[$k];
1886
            }
1887
        }
1888
1889
        if (RESOURCE_DOCUMENT === $type) {
1890
            $o->path = (string) ($o->path ?? $o->full_path ?? $o->obj->path ?? $o->obj->full_path ?? '');
1891
            $o->full_path = (string) ($o->full_path ?? $o->path ?? $o->obj->full_path ?? $o->obj->path ?? '');
1892
            $o->file_type = (string) ($o->file_type ?? $o->filetype ?? $o->obj->file_type ?? $o->obj->filetype ?? '');
1893
            $o->filetype = (string) ($o->filetype ?? $o->file_type ?? $o->obj->filetype ?? $o->obj->file_type ?? '');
1894
            $o->title = (string) ($o->title ?? $o->obj->title ?? '');
1895
            if (!isset($o->name) || '' === $o->name || null === $o->name) {
1896
                $o->name = '' !== $o->title ? $o->title : ('document '.$sourceId);
1897
            }
1898
        }
1899
1900
        if (RESOURCE_SURVEYQUESTION === $type) {
1901
            if (!isset($o->survey_question_type) && isset($o->type)) {
1902
                $o->survey_question_type = $o->type;
1903
            }
1904
            if (!isset($o->type) && isset($o->survey_question_type)) {
1905
                $o->type = $o->survey_question_type;
1906
            }
1907
1908
            if (isset($o->obj) && \is_object($o->obj)) {
1909
                if (!isset($o->obj->survey_question_type) && isset($o->obj->type)) {
1910
                    $o->obj->survey_question_type = $o->obj->type;
1911
                }
1912
                if (!isset($o->obj->type) && isset($o->obj->survey_question_type)) {
1913
                    $o->obj->type = $o->obj->survey_question_type;
1914
                }
1915
            }
1916
        }
1917
1918
        if (!isset($o->name) || '' === $o->name || null === $o->name) {
1919
            if (isset($obj['name']) && '' !== $obj['name']) {
1920
                $o->name = (string) $obj['name'];
1921
            } elseif (isset($obj['title']) && '' !== $obj['title']) {
1922
                $o->name = (string) $obj['title'];
1923
            } else {
1924
                $o->name = $type.' '.$sourceId;
1925
            }
1926
        }
1927
1928
        return $o;
1929
    }
1930
1931
    /**
1932
     * Build an id filter closure.
1933
     *
1934
     * @param array<int> $idsFilter
1935
     *
1936
     * @return Closure(int):bool
1937
     */
1938
    private function makeIdFilter(array $idsFilter): Closure
1939
    {
1940
        if (empty($idsFilter)) {
1941
            return static fn (int $id): bool => true;
1942
        }
1943
        $set = array_fill_keys(array_map('intval', $idsFilter), true);
1944
1945
        return static fn (int $id): bool => isset($set[$id]);
1946
    }
1947
1948
    /**
1949
     * Export Tool intro only for the course_homepage tool.
1950
     * Prefers the session-specific intro when both (session and base) exist.
1951
     */
1952
    private function build_tool_intro(
1953
        object $legacyCourse,
1954
        ?CourseEntity $courseEntity,
1955
        ?SessionEntity $sessionEntity
1956
    ): void {
1957
        if (!$courseEntity instanceof CourseEntity) {
1958
            return;
1959
        }
1960
1961
        $repo = $this->em->getRepository(CToolIntro::class);
1962
1963
        // Base query: join with tool, filter by course and the specific title
1964
        $qb = $repo->createQueryBuilder('ti')
1965
            ->innerJoin('ti.courseTool', 'ct')
1966
            ->andWhere('ct.course = :course')
1967
            ->andWhere('ct.title = :title')
1968
            ->setParameter('course', $courseEntity)
1969
            ->setParameter('title', 'course_homepage');
1970
1971
        // Session fallback: when a session is given, fetch both (session AND null)
1972
        if ($sessionEntity) {
1973
            $qb->andWhere($qb->expr()->orX('ct.session = :session', 'ct.session IS NULL'))
1974
                ->setParameter('session', $sessionEntity);
1975
        } else {
1976
            $qb->andWhere('ct.session IS NULL');
1977
        }
1978
1979
        /** @var CToolIntro[] $rows */
1980
        $rows = $qb->getQuery()->getResult();
1981
        if (!$rows) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rows of type Chamilo\CourseBundle\Entity\CToolIntro[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1982
            return; // Nothing to export
1983
        }
1984
1985
        // Prefer the session-bound intro if present; otherwise, use the base (session IS NULL)
1986
        $selected = null;
1987
        foreach ($rows as $row) {
1988
            $tool = $row->getCourseTool();
1989
            if ($sessionEntity && $tool && $tool->getSession()) {
1990
                $selected = $row; // session-specific hit
1991
                break;
1992
            }
1993
            if (!$selected) {
1994
                $selected = $row; // keep the base as fallback
1995
            }
1996
        }
1997
1998
        if (!$selected) {
1999
            return;
2000
        }
2001
2002
        $ctool = $selected->getCourseTool();
2003
        $titleKey = (string) ($ctool?->getTitle() ?? 'course_homepage');
2004
        if ($titleKey === '') {
2005
            $titleKey = 'course_homepage'; // safety net
2006
        }
2007
2008
        $payload = [
2009
            'id'         => $titleKey,
2010
            'intro_text' => (string) $selected->getIntroText(),
2011
        ];
2012
2013
        // Use 0 as source_id (unused by restore)
2014
        $legacyCourse->resources[RESOURCE_TOOL_INTRO][$titleKey] =
2015
            $this->mkLegacyItem(RESOURCE_TOOL_INTRO, 0, $payload);
2016
    }
2017
2018
    /**
2019
     * Export Forum categories.
2020
     *
2021
     * @param array<int> $ids
2022
     */
2023
    private function build_forum_category(
2024
        object $legacyCourse,
2025
        CourseEntity $courseEntity,
2026
        ?SessionEntity $sessionEntity,
2027
        array $ids
2028
    ): void {
2029
        $repo = Container::getForumCategoryRepository();
2030
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
2031
        $categories = $qb->getQuery()->getResult();
2032
2033
        $keep = $this->makeIdFilter($ids);
2034
2035
        foreach ($categories as $cat) {
2036
            /** @var CForumCategory $cat */
2037
            $id = (int) $cat->getIid();
2038
            if (!$keep($id)) {
2039
                continue;
2040
            }
2041
2042
            $payload = [
2043
                'title' => (string) $cat->getTitle(),
2044
                'description' => (string) ($cat->getCatComment() ?? ''),
2045
                'cat_title' => (string) $cat->getTitle(),
2046
                'cat_comment' => (string) ($cat->getCatComment() ?? ''),
2047
            ];
2048
2049
            $legacyCourse->resources[RESOURCE_FORUMCATEGORY][$id] =
2050
                $this->mkLegacyItem(RESOURCE_FORUMCATEGORY, $id, $payload);
2051
        }
2052
    }
2053
2054
    /**
2055
     * Export Forums.
2056
     *
2057
     * @param array<int> $ids
2058
     */
2059
    private function build_forums(
2060
        object $legacyCourse,
2061
        CourseEntity $courseEntity,
2062
        ?SessionEntity $sessionEntity,
2063
        array $ids
2064
    ): void {
2065
        $repo = Container::getForumRepository();
2066
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
2067
        $forums = $qb->getQuery()->getResult();
2068
2069
        $keep = $this->makeIdFilter($ids);
2070
2071
        foreach ($forums as $f) {
2072
            /** @var CForum $f */
2073
            $id = (int) $f->getIid();
2074
            if (!$keep($id)) {
2075
                continue;
2076
            }
2077
2078
            $payload = [
2079
                'title' => (string) $f->getTitle(),
2080
                'description' => (string) ($f->getForumComment() ?? ''),
2081
                'forum_title' => (string) $f->getTitle(),
2082
                'forum_comment' => (string) ($f->getForumComment() ?? ''),
2083
                'forum_category' => (int) ($f->getForumCategory()?->getIid() ?? 0),
2084
                'allow_anonymous' => (int) ($f->getAllowAnonymous() ?? 0),
2085
                'allow_edit' => (int) ($f->getAllowEdit() ?? 0),
2086
                'approval_direct_post' => (string) ($f->getApprovalDirectPost() ?? '0'),
2087
                'allow_attachments' => (int) ($f->getAllowAttachments() ?? 1),
2088
                'allow_new_threads' => (int) ($f->getAllowNewThreads() ?? 1),
2089
                'default_view' => (string) ($f->getDefaultView() ?? 'flat'),
2090
                'forum_of_group' => (string) ($f->getForumOfGroup() ?? '0'),
2091
                'forum_group_public_private' => (string) ($f->getForumGroupPublicPrivate() ?? 'public'),
2092
                'moderated' => (int) ($f->isModerated() ? 1 : 0),
2093
                'start_time' => $this->fmtDate($f->getStartTime()),
2094
                'end_time' => $this->fmtDate($f->getEndTime()),
2095
            ];
2096
2097
            $legacyCourse->resources[RESOURCE_FORUM][$id] =
2098
                $this->mkLegacyItem(RESOURCE_FORUM, $id, $payload);
2099
        }
2100
    }
2101
2102
    /**
2103
     * Export Forum threads.
2104
     *
2105
     * @param array<int> $ids
2106
     */
2107
    private function build_forum_topics(
2108
        object $legacyCourse,
2109
        CourseEntity $courseEntity,
2110
        ?SessionEntity $sessionEntity,
2111
        array $ids
2112
    ): void {
2113
        $repo = Container::getForumThreadRepository();
2114
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
2115
        $threads = $qb->getQuery()->getResult();
2116
2117
        $keep = $this->makeIdFilter($ids);
2118
2119
        foreach ($threads as $t) {
2120
            /** @var CForumThread $t */
2121
            $id = (int) $t->getIid();
2122
            if (!$keep($id)) {
2123
                continue;
2124
            }
2125
2126
            $payload = [
2127
                'title' => (string) $t->getTitle(),
2128
                'thread_title' => (string) $t->getTitle(),
2129
                'title_qualify' => (string) ($t->getThreadTitleQualify() ?? ''),
2130
                'topic_poster_name' => (string) ($t->getUser()?->getUsername() ?? ''),
2131
                'forum_id' => (int) ($t->getForum()?->getIid() ?? 0),
2132
                'thread_date' => $this->fmtDate($t->getThreadDate()),
2133
                'thread_sticky' => (int) ($t->getThreadSticky() ? 1 : 0),
2134
                'thread_title_qualify' => (string) ($t->getThreadTitleQualify() ?? ''),
2135
                'thread_qualify_max' => (float) $t->getThreadQualifyMax(),
2136
                'thread_weight' => (float) $t->getThreadWeight(),
2137
                'thread_peer_qualify' => (int) ($t->isThreadPeerQualify() ? 1 : 0),
2138
            ];
2139
2140
            $legacyCourse->resources[RESOURCE_FORUMTOPIC][$id] =
2141
                $this->mkLegacyItem(RESOURCE_FORUMTOPIC, $id, $payload);
2142
        }
2143
    }
2144
2145
    /**
2146
     * Export first post for each thread as topic root post.
2147
     *
2148
     * @param array<int> $ids
2149
     */
2150
    private function build_forum_posts(
2151
        object $legacyCourse,
2152
        CourseEntity $courseEntity,
2153
        ?SessionEntity $sessionEntity,
2154
        array $ids
2155
    ): void {
2156
        $repoThread = Container::getForumThreadRepository();
2157
        $repoPost = Container::getForumPostRepository();
2158
2159
        $qb = $repoThread->getResourcesByCourse($courseEntity, $sessionEntity);
2160
        $threads = $qb->getQuery()->getResult();
2161
2162
        $keep = $this->makeIdFilter($ids);
2163
2164
        foreach ($threads as $t) {
2165
            /** @var CForumThread $t */
2166
            $threadId = (int) $t->getIid();
2167
            if (!$keep($threadId)) {
2168
                continue;
2169
            }
2170
2171
            $first = $repoPost->findOneBy(['thread' => $t], ['postDate' => 'ASC', 'iid' => 'ASC']);
2172
            if (!$first) {
2173
                continue;
2174
            }
2175
2176
            $postId = (int) $first->getIid();
2177
            $titleFromPost = trim((string) $first->getTitle());
2178
            if ('' === $titleFromPost) {
2179
                $plain = trim(strip_tags((string) ($first->getPostText() ?? '')));
2180
                $titleFromPost = mb_substr('' !== $plain ? $plain : 'Post', 0, 60);
2181
            }
2182
2183
            $payload = [
2184
                'title' => $titleFromPost,
2185
                'post_title' => $titleFromPost,
2186
                'post_text' => (string) ($first->getPostText() ?? ''),
2187
                'thread_id' => $threadId,
2188
                'forum_id' => (int) ($t->getForum()?->getIid() ?? 0),
2189
                'post_notification' => (int) ($first->getPostNotification() ? 1 : 0),
2190
                'visible' => (int) ($first->getVisible() ? 1 : 0),
2191
                'status' => (int) ($first->getStatus() ?? CForumPost::STATUS_VALIDATED),
2192
                'post_parent_id' => (int) ($first->getPostParent()?->getIid() ?? 0),
2193
                'poster_id' => (int) ($first->getUser()?->getId() ?? 0),
2194
                'text' => (string) ($first->getPostText() ?? ''),
2195
                'poster_name' => (string) ($first->getUser()?->getUsername() ?? ''),
2196
                'post_date' => $this->fmtDate($first->getPostDate()),
2197
            ];
2198
2199
            $legacyCourse->resources[RESOURCE_FORUMPOST][$postId] =
2200
                $this->mkLegacyItem(RESOURCE_FORUMPOST, $postId, $payload);
2201
        }
2202
    }
2203
2204
    /**
2205
     * New Chamilo 2 build: CDocumentRepository-based (instead of legacy tables).
2206
     *
2207
     * @param array<int> $idList
2208
     */
2209
    private function build_documents_with_repo(
2210
        ?CourseEntity $course,
2211
        ?SessionEntity $session,
2212
        bool $withBaseContent,
2213
        array $idList = []
2214
    ): void {
2215
        if (!$course instanceof CourseEntity) {
2216
            return;
2217
        }
2218
2219
        $qb = $this->docRepo->getResourcesByCourse($course, $session, null, null, true, false);
2220
2221
        if (!empty($idList)) {
2222
            $qb->andWhere('resource.iid IN (:ids)')
2223
                ->setParameter('ids', array_values(array_unique(array_map('intval', $idList))));
2224
        }
2225
2226
        /** @var CDocument[] $docs */
2227
        $docs = $qb->getQuery()->getResult();
2228
2229
        $documentsRoot = $this->docRepo->getCourseDocumentsRootNode($course);
2230
2231
        foreach ($docs as $doc) {
2232
            $node     = $doc->getResourceNode();
2233
            $filetype = $doc->getFiletype(); // 'file' | 'folder' | ...
2234
            $title    = $doc->getTitle();
2235
            $comment  = $doc->getComment() ?? '';
2236
            $iid      = (int) $doc->getIid();
2237
2238
            $size = 0;
2239
            if ('folder' === $filetype) {
2240
                $size = $this->docRepo->getFolderSize($node, $course, $session);
2241
            } else {
2242
                $files = $node?->getResourceFiles();
2243
                if ($files && $files->count() > 0) {
2244
                    /** @var ResourceFile $first */
2245
                    $first = $files->first();
2246
                    $size  = (int) $first->getSize();
2247
                }
2248
            }
2249
2250
            $rel = '';
2251
            if ($node instanceof ResourceNode) {
2252
                if ($documentsRoot instanceof ResourceNode) {
2253
                    $rel = $node->getPathForDisplayRemoveBase((string) $documentsRoot->getPath()); // e.g. "folder001/file.ext"
2254
                } else {
2255
                    $rel = $node->convertPathForDisplay((string) $node->getPath()); // e.g. "Documents/folder001/file.ext"
2256
                    $rel = preg_replace('~^/?Documents/?~i', '', (string) $rel) ?? $rel;
2257
                }
2258
            }
2259
            $rel = trim((string) $rel, '/');
2260
            $pathForSelector = 'document' . ($rel !== '' ? '/'.$rel : '');
2261
            if ($filetype === 'folder') {
2262
                $pathForSelector = rtrim($pathForSelector, '/').'/';
2263
            }
2264
2265
            $exportDoc = new Document(
2266
                $iid,
2267
                $pathForSelector,
2268
                $comment,
2269
                $title,
2270
                $filetype,
2271
                (string) $size
2272
            );
2273
2274
            $this->course->add_resource($exportDoc);
2275
        }
2276
    }
2277
2278
    /**
2279
     * Backward-compatible wrapper for build_documents_with_repo().
2280
     *
2281
     * @param array<int> $idList
2282
     */
2283
    public function build_documents(
2284
        int $session_id = 0,
2285
        int $courseId = 0,
2286
        bool $withBaseContent = false,
2287
        array $idList = []
2288
    ): void {
2289
        /** @var CourseEntity|null $course */
2290
        $course = $this->em->getRepository(CourseEntity::class)->find($courseId);
2291
2292
        /** @var SessionEntity|null $session */
2293
        $session = $session_id ? $this->em->getRepository(SessionEntity::class)->find($session_id) : null;
2294
2295
        $this->build_documents_with_repo($course, $session, $withBaseContent, $idList);
2296
    }
2297
}
2298