Passed
Push — master ( 68f627...939207 )
by
unknown
12:17 queued 03:34
created

CourseBuilder::mkLegacyItem()   F

Complexity

Conditions 34
Paths > 20000

Size

Total Lines 72
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 34
eloc 45
c 0
b 0
f 0
nc 32256
nop 4
dl 0
loc 72
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
0 ignored issues
show
Bug introduced by
The property info does not seem to exist on Chamilo\CourseBundle\Component\CourseCopy\Course.
Loading history...
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
346
        foreach ($this->tools_to_build as $toolKey) {
347
            if (!empty($parseOnlyToolList)) {
348
                $const = $this->toolToName[$toolKey] ?? null;
349
                if (null !== $const && !\in_array($const, $parseOnlyToolList, true)) {
350
                    continue;
351
                }
352
            }
353
354
            if ('documents' === $toolKey) {
355
                $ids = $this->specific_id_list['documents'] ?? [];
356
                $this->build_documents_with_repo($courseEntity, $sessionEntity, $withBaseContent, $ids);
357
            }
358
359
            if ('forums' === $toolKey || 'forum' === $toolKey) {
360
                $ids = $this->specific_id_list['forums'] ?? $this->specific_id_list['forum'] ?? [];
361
                $this->build_forum_category($legacyCourse, $courseEntity, $sessionEntity, $ids);
362
                $this->build_forums($legacyCourse, $courseEntity, $sessionEntity, $ids);
363
                $this->build_forum_topics($legacyCourse, $courseEntity, $sessionEntity, $ids);
364
                $this->build_forum_posts($legacyCourse, $courseEntity, $sessionEntity, $ids);
365
            }
366
367
            if ('tool_intro' === $toolKey) {
368
                $this->build_tool_intro($legacyCourse, $courseEntity, $sessionEntity);
369
            }
370
371
            if ('links' === $toolKey) {
372
                $ids = $this->specific_id_list['links'] ?? [];
373
                $this->build_links($legacyCourse, $courseEntity, $sessionEntity, $ids);
374
            }
375
376
            if ('quizzes' === $toolKey || 'quiz' === $toolKey) {
377
                $ids = $this->specific_id_list['quizzes'] ?? $this->specific_id_list['quiz'] ?? [];
378
                $neededQuestionIds = $this->build_quizzes($legacyCourse, $courseEntity, $sessionEntity, $ids);
379
                // Always export question bucket required by the quizzes
380
                $this->build_quiz_questions($legacyCourse, $courseEntity, $sessionEntity, $neededQuestionIds);
381
                error_log(
382
                    'COURSE_BUILD: quizzes='.\count($legacyCourse->resources[RESOURCE_QUIZ] ?? []).
383
                    ' quiz_questions='.\count($legacyCourse->resources[RESOURCE_QUIZQUESTION] ?? [])
384
                );
385
            }
386
387
            if ('quiz_questions' === $toolKey) {
388
                $ids = $this->specific_id_list['quiz_questions'] ?? [];
389
                $this->build_quiz_questions($legacyCourse, $courseEntity, $sessionEntity, $ids);
390
                error_log(
391
                    'COURSE_BUILD: explicit quiz_questions='.\count($legacyCourse->resources[RESOURCE_QUIZQUESTION] ?? [])
392
                );
393
            }
394
395
            if ('surveys' === $toolKey || 'survey' === $toolKey) {
396
                $ids = $this->specific_id_list['surveys'] ?? $this->specific_id_list['survey'] ?? [];
397
                $neededQ = $this->build_surveys($this->course, $courseEntity, $sessionEntity, $ids);
398
                $this->build_survey_questions($this->course, $courseEntity, $sessionEntity, $neededQ);
399
            }
400
401
            if ('survey_questions' === $toolKey) {
402
                $this->build_survey_questions($this->course, $courseEntity, $sessionEntity, []);
403
            }
404
405
            if ('announcements' === $toolKey) {
406
                $ids = $this->specific_id_list['announcements'] ?? [];
407
                $this->build_announcements($this->course, $courseEntity, $sessionEntity, $ids);
408
            }
409
410
            if ('events' === $toolKey) {
411
                $ids = $this->specific_id_list['events'] ?? [];
412
                $this->build_events($this->course, $courseEntity, $sessionEntity, $ids);
413
            }
414
415
            if ('course_descriptions' === $toolKey) {
416
                $ids = $this->specific_id_list['course_descriptions'] ?? [];
417
                $this->build_course_descriptions($this->course, $courseEntity, $sessionEntity, $ids);
418
            }
419
420
            if ('glossary' === $toolKey) {
421
                $ids = $this->specific_id_list['glossary'] ?? [];
422
                $this->build_glossary($this->course, $courseEntity, $sessionEntity, $ids);
423
            }
424
425
            if ('wiki' === $toolKey) {
426
                $ids = $this->specific_id_list['wiki'] ?? [];
427
                $this->build_wiki($this->course, $courseEntity, $sessionEntity, $ids);
428
            }
429
430
            if ('thematic' === $toolKey) {
431
                $ids = $this->specific_id_list['thematic'] ?? [];
432
                $this->build_thematic($this->course, $courseEntity, $sessionEntity, $ids);
433
            }
434
435
            if ('attendance' === $toolKey) {
436
                $ids = $this->specific_id_list['attendance'] ?? [];
437
                $this->build_attendance($this->course, $courseEntity, $sessionEntity, $ids);
438
            }
439
440
            if ('works' === $toolKey) {
441
                $ids = $this->specific_id_list['works'] ?? [];
442
                $this->build_works($this->course, $courseEntity, $sessionEntity, $ids);
443
            }
444
445
            if ('gradebook' === $toolKey) {
446
                $this->build_gradebook($this->course, $courseEntity, $sessionEntity);
447
            }
448
449
            if ('learnpath_category' === $toolKey) {
450
                $ids = $this->specific_id_list['learnpath_category'] ?? [];
451
                $this->build_learnpath_category($this->course, $courseEntity, $sessionEntity, $ids);
452
            }
453
454
            if ('learnpaths' === $toolKey) {
455
                $ids = $this->specific_id_list['learnpaths'] ?? [];
456
                $this->build_learnpaths($this->course, $courseEntity, $sessionEntity, $ids, true);
457
            }
458
        }
459
460
        return $this->course;
461
    }
462
463
    /**
464
     * Export Learnpath categories (CLpCategory).
465
     *
466
     * @param array<int> $ids
467
     */
468
    private function build_learnpath_category(
469
        object $legacyCourse,
470
        ?CourseEntity $courseEntity,
471
        ?SessionEntity $sessionEntity,
472
        array $ids
473
    ): void {
474
        if (!$courseEntity instanceof CourseEntity) {
475
            return;
476
        }
477
478
        $repo = Container::getLpCategoryRepository();
479
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
480
481
        if (!empty($ids)) {
482
            $qb->andWhere('resource.iid IN (:ids)')
483
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
484
            ;
485
        }
486
487
        /** @var CLpCategory[] $rows */
488
        $rows = $qb->getQuery()->getResult();
489
490
        foreach ($rows as $cat) {
491
            $iid = (int) $cat->getIid();
492
            $title = (string) $cat->getTitle();
493
494
            $payload = [
495
                'id' => $iid,
496
                'title' => $title,
497
            ];
498
499
            $legacyCourse->resources[RESOURCE_LEARNPATH_CATEGORY][$iid] =
500
                $this->mkLegacyItem(RESOURCE_LEARNPATH_CATEGORY, $iid, $payload);
501
        }
502
    }
503
504
    /**
505
     * Export Learnpaths (CLp) + items, with optional SCORM folder packing.
506
     *
507
     * @param array<int> $idList
508
     */
509
    private function build_learnpaths(
510
        object $legacyCourse,
511
        ?CourseEntity $courseEntity,
512
        ?SessionEntity $sessionEntity,
513
        array $idList = [],
514
        bool $addScormFolder = true
515
    ): void {
516
        if (!$courseEntity instanceof CourseEntity) {
517
            return;
518
        }
519
520
        $lpRepo = Container::getLpRepository();
521
        $qb = $lpRepo->getResourcesByCourse($courseEntity, $sessionEntity);
522
523
        if (!empty($idList)) {
524
            $qb->andWhere('resource.iid IN (:ids)')
525
                ->setParameter('ids', array_values(array_unique(array_map('intval', $idList))))
526
            ;
527
        }
528
529
        /** @var CLp[] $lps */
530
        $lps = $qb->getQuery()->getResult();
531
532
        foreach ($lps as $lp) {
533
            $iid = (int) $lp->getIid();
534
            $lpType = (int) $lp->getLpType(); // 1=LP, 2=SCORM, 3=AICC
535
536
            $items = [];
537
538
            /** @var CLpItem $it */
539
            foreach ($lp->getItems() as $it) {
540
                $items[] = [
541
                    'id' => (int) $it->getIid(),
542
                    'item_type' => (string) $it->getItemType(),
543
                    'ref' => (string) $it->getRef(),
544
                    'title' => (string) $it->getTitle(),
545
                    'name' => (string) $lp->getTitle(),
546
                    'description' => (string) ($it->getDescription() ?? ''),
547
                    'path' => (string) $it->getPath(),
548
                    'min_score' => (float) $it->getMinScore(),
549
                    'max_score' => null !== $it->getMaxScore() ? (float) $it->getMaxScore() : null,
550
                    'mastery_score' => null !== $it->getMasteryScore() ? (float) $it->getMasteryScore() : null,
551
                    'parent_item_id' => (int) $it->getParentItemId(),
552
                    'previous_item_id' => null !== $it->getPreviousItemId() ? (int) $it->getPreviousItemId() : null,
553
                    'next_item_id' => null !== $it->getNextItemId() ? (int) $it->getNextItemId() : null,
554
                    'display_order' => (int) $it->getDisplayOrder(),
555
                    'prerequisite' => (string) ($it->getPrerequisite() ?? ''),
556
                    'parameters' => (string) ($it->getParameters() ?? ''),
557
                    'launch_data' => (string) $it->getLaunchData(),
558
                    'audio' => (string) ($it->getAudio() ?? ''),
559
                ];
560
            }
561
562
            $payload = [
563
                'id' => $iid,
564
                'lp_type' => $lpType,
565
                'title' => (string) $lp->getTitle(),
566
                'path' => (string) $lp->getPath(),
567
                'ref' => (string) ($lp->getRef() ?? ''),
568
                'description' => (string) ($lp->getDescription() ?? ''),
569
                'content_local' => (string) $lp->getContentLocal(),
570
                'default_encoding' => (string) $lp->getDefaultEncoding(),
571
                'default_view_mod' => (string) $lp->getDefaultViewMod(),
572
                'prevent_reinit' => (bool) $lp->getPreventReinit(),
573
                'force_commit' => (bool) $lp->getForceCommit(),
574
                'content_maker' => (string) $lp->getContentMaker(),
575
                'display_order' => (int) $lp->getDisplayNotAllowedLp(),
576
                'js_lib' => (string) $lp->getJsLib(),
577
                'content_license' => (string) $lp->getContentLicense(),
578
                'debug' => (bool) $lp->getDebug(),
579
                'visibility' => '1',
580
                'author' => (string) $lp->getAuthor(),
581
                'use_max_score' => (int) $lp->getUseMaxScore(),
582
                'autolaunch' => (int) $lp->getAutolaunch(),
583
                'created_on' => $this->fmtDate($lp->getCreatedOn()),
584
                'modified_on' => $this->fmtDate($lp->getModifiedOn()),
585
                'published_on' => $this->fmtDate($lp->getPublishedOn()),
586
                'expired_on' => $this->fmtDate($lp->getExpiredOn()),
587
                'session_id' => (int) ($sessionEntity?->getId() ?? 0),
588
                'category_id' => (int) ($lp->getCategory()?->getIid() ?? 0),
589
                'items' => $items,
590
            ];
591
592
            $legacyCourse->resources[RESOURCE_LEARNPATH][$iid] =
593
                $this->mkLegacyItem(RESOURCE_LEARNPATH, $iid, $payload, ['items']);
594
        }
595
596
        // Optional: pack “scorm” folder (legacy parity)
597
        if ($addScormFolder && isset($this->course->backup_path)) {
598
            $scormDir = rtrim((string) $this->course->backup_path, '/').'/scorm';
599
            if (is_dir($scormDir) && ($dh = @opendir($scormDir))) {
600
                $i = 1;
601
                while (false !== ($file = readdir($dh))) {
602
                    if ('.' === $file || '..' === $file) {
603
                        continue;
604
                    }
605
                    if (is_dir($scormDir.'/'.$file)) {
606
                        $payload = ['path' => '/'.$file, 'name' => (string) $file];
607
                        $legacyCourse->resources['scorm'][$i] =
608
                            $this->mkLegacyItem('scorm', $i, $payload);
609
                        $i++;
610
                    }
611
                }
612
                closedir($dh);
613
            }
614
        }
615
    }
616
617
    /**
618
     * Export Gradebook (categories + evaluations + links).
619
     */
620
    private function build_gradebook(
621
        object $legacyCourse,
622
        ?CourseEntity $courseEntity,
623
        ?SessionEntity $sessionEntity
624
    ): void {
625
        if (!$courseEntity instanceof CourseEntity) {
626
            return;
627
        }
628
629
        /** @var EntityManagerInterface $em */
630
        $em = Database::getManager();
631
        $catRepo = $em->getRepository(GradebookCategory::class);
632
633
        $criteria = ['course' => $courseEntity];
634
        if ($sessionEntity) {
635
            $criteria['session'] = $sessionEntity;
636
        }
637
638
        /** @var GradebookCategory[] $cats */
639
        $cats = $catRepo->findBy($criteria);
640
        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...
641
            return;
642
        }
643
644
        $payloadCategories = [];
645
        foreach ($cats as $cat) {
646
            $payloadCategories[] = $this->serializeGradebookCategory($cat);
647
        }
648
649
        $backup = new GradeBookBackup($payloadCategories);
650
        $legacyCourse->add_resource($backup);
651
    }
652
653
    /**
654
     * Serialize GradebookCategory (and nested parts) to array for restore.
655
     *
656
     * @return array<string,mixed>
657
     */
658
    private function serializeGradebookCategory(GradebookCategory $c): array
659
    {
660
        $arr = [
661
            'id' => (int) $c->getId(),
662
            'title' => (string) $c->getTitle(),
663
            'description' => (string) ($c->getDescription() ?? ''),
664
            'weight' => (float) $c->getWeight(),
665
            'visible' => (bool) $c->getVisible(),
666
            'locked' => (int) $c->getLocked(),
667
            'parent_id' => $c->getParent() ? (int) $c->getParent()->getId() : 0,
668
            'generate_certificates' => (bool) $c->getGenerateCertificates(),
669
            'certificate_validity_period' => $c->getCertificateValidityPeriod(),
670
            'is_requirement' => (bool) $c->getIsRequirement(),
671
            'default_lowest_eval_exclude' => (bool) $c->getDefaultLowestEvalExclude(),
672
            'minimum_to_validate' => $c->getMinimumToValidate(),
673
            'gradebooks_to_validate_in_dependence' => $c->getGradeBooksToValidateInDependence(),
674
            'allow_skills_by_subcategory' => $c->getAllowSkillsBySubcategory(),
675
            // camelCase duplicates (future-proof)
676
            'generateCertificates' => (bool) $c->getGenerateCertificates(),
677
            'certificateValidityPeriod' => $c->getCertificateValidityPeriod(),
678
            'isRequirement' => (bool) $c->getIsRequirement(),
679
            'defaultLowestEvalExclude' => (bool) $c->getDefaultLowestEvalExclude(),
680
            'minimumToValidate' => $c->getMinimumToValidate(),
681
            'gradeBooksToValidateInDependence' => $c->getGradeBooksToValidateInDependence(),
682
            'allowSkillsBySubcategory' => $c->getAllowSkillsBySubcategory(),
683
        ];
684
685
        if ($c->getGradeModel()) {
686
            $arr['grade_model_id'] = (int) $c->getGradeModel()->getId();
687
        }
688
689
        // Evaluations
690
        $arr['evaluations'] = [];
691
        foreach ($c->getEvaluations() as $e) {
692
            /** @var GradebookEvaluation $e */
693
            $arr['evaluations'][] = [
694
                'title' => (string) $e->getTitle(),
695
                'description' => (string) ($e->getDescription() ?? ''),
696
                'weight' => (float) $e->getWeight(),
697
                'max' => (float) $e->getMax(),
698
                'type' => (string) $e->getType(),
699
                'visible' => (int) $e->getVisible(),
700
                'locked' => (int) $e->getLocked(),
701
                'best_score' => $e->getBestScore(),
702
                'average_score' => $e->getAverageScore(),
703
                'score_weight' => $e->getScoreWeight(),
704
                'min_score' => $e->getMinScore(),
705
            ];
706
        }
707
708
        // Links
709
        $arr['links'] = [];
710
        foreach ($c->getLinks() as $l) {
711
            /** @var GradebookLink $l */
712
            $arr['links'][] = [
713
                'type' => (int) $l->getType(),
714
                'ref_id' => (int) $l->getRefId(),
715
                'weight' => (float) $l->getWeight(),
716
                'visible' => (int) $l->getVisible(),
717
                'locked' => (int) $l->getLocked(),
718
                'best_score' => $l->getBestScore(),
719
                'average_score' => $l->getAverageScore(),
720
                'score_weight' => $l->getScoreWeight(),
721
                'min_score' => $l->getMinScore(),
722
            ];
723
        }
724
725
        return $arr;
726
    }
727
728
    /**
729
     * Export Works (root folders only; include assignment params).
730
     *
731
     * @param array<int> $ids
732
     */
733
    private function build_works(
734
        object $legacyCourse,
735
        ?CourseEntity $courseEntity,
736
        ?SessionEntity $sessionEntity,
737
        array $ids
738
    ): void {
739
        if (!$courseEntity instanceof CourseEntity) {
740
            return;
741
        }
742
743
        $repo = Container::getStudentPublicationRepository();
744
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
745
746
        $qb
747
            ->andWhere('resource.publicationParent IS NULL')
748
            ->andWhere('resource.filetype = :ft')->setParameter('ft', 'folder')
749
            ->andWhere('resource.active = 1')
750
        ;
751
752
        if (!empty($ids)) {
753
            $qb->andWhere('resource.iid IN (:ids)')
754
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
755
            ;
756
        }
757
758
        /** @var CStudentPublication[] $rows */
759
        $rows = $qb->getQuery()->getResult();
760
761
        foreach ($rows as $row) {
762
            $iid = (int) $row->getIid();
763
            $title = (string) $row->getTitle();
764
            $desc = (string) ($row->getDescription() ?? '');
765
766
            // Detect documents linked in description
767
            $this->findAndSetDocumentsInText($desc);
768
769
            $asgmt = $row->getAssignment();
770
            $expiresOn = $asgmt?->getExpiresOn()?->format('Y-m-d H:i:s');
771
            $endsOn = $asgmt?->getEndsOn()?->format('Y-m-d H:i:s');
772
            $addToCal = $asgmt && $asgmt->getEventCalendarId() > 0 ? 1 : 0;
773
            $enableQ = (bool) ($asgmt?->getEnableQualification() ?? false);
774
775
            $params = [
776
                'id' => $iid,
777
                'title' => $title,
778
                'description' => $desc,
779
                'weight' => (float) $row->getWeight(),
780
                'qualification' => (float) $row->getQualification(),
781
                'allow_text_assignment' => (int) $row->getAllowTextAssignment(),
782
                'default_visibility' => (bool) ($row->getDefaultVisibility() ?? false),
783
                'student_delete_own_publication' => (bool) ($row->getStudentDeleteOwnPublication() ?? false),
784
                'extensions' => $row->getExtensions(),
785
                'group_category_work_id' => (int) $row->getGroupCategoryWorkId(),
786
                'post_group_id' => (int) $row->getPostGroupId(),
787
                'enable_qualification' => $enableQ,
788
                'add_to_calendar' => $addToCal ? 1 : 0,
789
                'expires_on' => $expiresOn ?: null,
790
                'ends_on' => $endsOn ?: null,
791
                'name' => $title,
792
                'url' => null,
793
            ];
794
795
            $legacy = new Work($params);
796
            $legacyCourse->add_resource($legacy);
797
        }
798
    }
799
800
    /**
801
     * Export Attendance + calendars.
802
     *
803
     * @param array<int> $ids
804
     */
805
    private function build_attendance(
806
        object $legacyCourse,
807
        ?CourseEntity $courseEntity,
808
        ?SessionEntity $sessionEntity,
809
        array $ids
810
    ): void {
811
        if (!$courseEntity instanceof CourseEntity) {
812
            return;
813
        }
814
815
        $repo = Container::getAttendanceRepository();
816
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
817
818
        if (!empty($ids)) {
819
            $qb->andWhere('resource.iid IN (:ids)')
820
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
821
            ;
822
        }
823
824
        /** @var CAttendance[] $rows */
825
        $rows = $qb->getQuery()->getResult();
826
827
        foreach ($rows as $row) {
828
            $iid = (int) $row->getIid();
829
            $title = (string) $row->getTitle();
830
            $desc = (string) ($row->getDescription() ?? '');
831
            $active = (int) $row->getActive();
832
833
            $this->findAndSetDocumentsInText($desc);
834
835
            $params = [
836
                'id' => $iid,
837
                'title' => $title,
838
                'description' => $desc,
839
                'active' => $active,
840
                'attendance_qualify_title' => (string) ($row->getAttendanceQualifyTitle() ?? ''),
841
                'attendance_qualify_max' => (int) $row->getAttendanceQualifyMax(),
842
                'attendance_weight' => (float) $row->getAttendanceWeight(),
843
                'locked' => (int) $row->getLocked(),
844
                'name' => $title,
845
            ];
846
847
            $legacy = new Attendance($params);
848
849
            /** @var CAttendanceCalendar $cal */
850
            foreach ($row->getCalendars() as $cal) {
851
                $calArr = [
852
                    'id' => (int) $cal->getIid(),
853
                    'attendance_id' => $iid,
854
                    'date_time' => $cal->getDateTime()?->format('Y-m-d H:i:s') ?? '',
855
                    'done_attendance' => (bool) $cal->getDoneAttendance(),
856
                    'blocked' => (bool) $cal->getBlocked(),
857
                    'duration' => null !== $cal->getDuration() ? (int) $cal->getDuration() : null,
858
                ];
859
                $legacy->add_attendance_calendar($calArr);
860
            }
861
862
            $legacyCourse->add_resource($legacy);
863
        }
864
    }
865
866
    /**
867
     * Export Thematic + advances + plans (and collect linked docs).
868
     *
869
     * @param array<int> $ids
870
     */
871
    private function build_thematic(
872
        object $legacyCourse,
873
        ?CourseEntity $courseEntity,
874
        ?SessionEntity $sessionEntity,
875
        array $ids
876
    ): void {
877
        if (!$courseEntity instanceof CourseEntity) {
878
            return;
879
        }
880
881
        $repo = Container::getThematicRepository();
882
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
883
884
        if (!empty($ids)) {
885
            $qb->andWhere('resource.iid IN (:ids)')
886
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
887
            ;
888
        }
889
890
        /** @var CThematic[] $rows */
891
        $rows = $qb->getQuery()->getResult();
892
893
        foreach ($rows as $row) {
894
            $iid = (int) $row->getIid();
895
            $title = (string) $row->getTitle();
896
            $content = (string) ($row->getContent() ?? '');
897
            $active = (bool) $row->getActive();
898
899
            $this->findAndSetDocumentsInText($content);
900
901
            $params = [
902
                'id' => $iid,
903
                'title' => $title,
904
                'content' => $content,
905
                'active' => $active,
906
            ];
907
908
            $legacy = new Thematic($params);
909
910
            /** @var CThematicAdvance $adv */
911
            foreach ($row->getAdvances() as $adv) {
912
                $attendanceId = 0;
913
914
                try {
915
                    $refAtt = new ReflectionProperty(CThematicAdvance::class, 'attendance');
916
                    if ($refAtt->isInitialized($adv)) {
917
                        $att = $adv->getAttendance();
918
                        if ($att) {
919
                            $attendanceId = (int) $att->getIid();
920
                        }
921
                    }
922
                } catch (Throwable) {
923
                    // keep $attendanceId = 0
924
                }
925
926
                $advArr = [
927
                    'id' => (int) $adv->getIid(),
928
                    'thematic_id' => (int) $row->getIid(),
929
                    'content' => (string) ($adv->getContent() ?? ''),
930
                    'start_date' => $adv->getStartDate()?->format('Y-m-d H:i:s') ?? '',
931
                    'duration' => (int) $adv->getDuration(),
932
                    'done_advance' => (bool) $adv->getDoneAdvance(),
933
                    'attendance_id' => $attendanceId,
934
                    'room_id' => (int) ($adv->getRoom()?->getId() ?? 0),
935
                ];
936
937
                $this->findAndSetDocumentsInText((string) $advArr['content']);
938
                $legacy->addThematicAdvance($advArr);
939
            }
940
941
            /** @var CThematicPlan $pl */
942
            foreach ($row->getPlans() as $pl) {
943
                $plArr = [
944
                    'id' => (int) $pl->getIid(),
945
                    'thematic_id' => $iid,
946
                    'title' => (string) $pl->getTitle(),
947
                    'description' => (string) ($pl->getDescription() ?? ''),
948
                    'description_type' => (int) $pl->getDescriptionType(),
949
                ];
950
                $this->findAndSetDocumentsInText((string) $plArr['description']);
951
                $legacy->addThematicPlan($plArr);
952
            }
953
954
            $legacyCourse->add_resource($legacy);
955
        }
956
    }
957
958
    /**
959
     * Export Wiki pages (content + metadata; collect docs in content).
960
     *
961
     * @param array<int> $ids
962
     */
963
    private function build_wiki(
964
        object $legacyCourse,
965
        ?CourseEntity $courseEntity,
966
        ?SessionEntity $sessionEntity,
967
        array $ids
968
    ): void {
969
        if (!$courseEntity instanceof CourseEntity) {
970
            return;
971
        }
972
973
        $repo = Container::getWikiRepository();
974
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
975
976
        if (!empty($ids)) {
977
            $qb->andWhere('resource.iid IN (:ids)')
978
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
979
            ;
980
        }
981
982
        /** @var CWiki[] $pages */
983
        $pages = $qb->getQuery()->getResult();
984
985
        foreach ($pages as $page) {
986
            $iid = (int) $page->getIid();
987
            $pageId = (int) ($page->getPageId() ?? $iid);
988
            $reflink = (string) $page->getReflink();
989
            $title = (string) $page->getTitle();
990
            $content = (string) $page->getContent();
991
            $userId = (int) $page->getUserId();
992
            $groupId = (int) ($page->getGroupId() ?? 0);
993
            $progress = (string) ($page->getProgress() ?? '');
994
            $version = (int) ($page->getVersion() ?? 1);
995
            $dtime = $page->getDtime()?->format('Y-m-d H:i:s') ?? '';
996
997
            $this->findAndSetDocumentsInText($content);
998
999
            $legacy = new Wiki(
1000
                $iid,
1001
                $pageId,
1002
                $reflink,
1003
                $title,
1004
                $content,
1005
                $userId,
1006
                $groupId,
1007
                $dtime,
1008
                $progress,
1009
                $version
1010
            );
1011
1012
            $this->course->add_resource($legacy);
1013
        }
1014
    }
1015
1016
    /**
1017
     * Export Glossary terms (collect docs in descriptions).
1018
     *
1019
     * @param array<int> $ids
1020
     */
1021
    private function build_glossary(
1022
        object $legacyCourse,
1023
        ?CourseEntity $courseEntity,
1024
        ?SessionEntity $sessionEntity,
1025
        array $ids
1026
    ): void {
1027
        if (!$courseEntity instanceof CourseEntity) {
1028
            return;
1029
        }
1030
1031
        $repo = Container::getGlossaryRepository();
1032
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
1033
1034
        if (!empty($ids)) {
1035
            $qb->andWhere('resource.iid IN (:ids)')
1036
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1037
            ;
1038
        }
1039
1040
        /** @var CGlossary[] $terms */
1041
        $terms = $qb->getQuery()->getResult();
1042
1043
        foreach ($terms as $term) {
1044
            $iid = (int) $term->getIid();
1045
            $title = (string) $term->getTitle();
1046
            $desc = (string) ($term->getDescription() ?? '');
1047
1048
            $this->findAndSetDocumentsInText($desc);
1049
1050
            $legacy = new Glossary(
1051
                $iid,
1052
                $title,
1053
                $desc,
1054
                0
1055
            );
1056
1057
            $this->course->add_resource($legacy);
1058
        }
1059
    }
1060
1061
    /**
1062
     * Export Course descriptions (collect docs in HTML).
1063
     *
1064
     * @param array<int> $ids
1065
     */
1066
    private function build_course_descriptions(
1067
        object $legacyCourse,
1068
        ?CourseEntity $courseEntity,
1069
        ?SessionEntity $sessionEntity,
1070
        array $ids
1071
    ): void {
1072
        if (!$courseEntity instanceof CourseEntity) {
1073
            return;
1074
        }
1075
1076
        $repo = Container::getCourseDescriptionRepository();
1077
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
1078
1079
        if (!empty($ids)) {
1080
            $qb->andWhere('resource.iid IN (:ids)')
1081
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1082
            ;
1083
        }
1084
1085
        /** @var CCourseDescription[] $rows */
1086
        $rows = $qb->getQuery()->getResult();
1087
1088
        foreach ($rows as $row) {
1089
            $iid = (int) $row->getIid();
1090
            $title = (string) ($row->getTitle() ?? '');
1091
            $html = (string) ($row->getContent() ?? '');
1092
            $type = (int) $row->getDescriptionType();
1093
1094
            $this->findAndSetDocumentsInText($html);
1095
1096
            $export = new CourseDescription(
1097
                $iid,
1098
                $title,
1099
                $html,
1100
                $type
1101
            );
1102
1103
            $this->course->add_resource($export);
1104
        }
1105
    }
1106
1107
    /**
1108
     * Export Calendar events (first attachment as legacy, all as assets).
1109
     *
1110
     * @param array<int> $ids
1111
     */
1112
    private function build_events(
1113
        object $legacyCourse,
1114
        ?CourseEntity $courseEntity,
1115
        ?SessionEntity $sessionEntity,
1116
        array $ids
1117
    ): void {
1118
        if (!$courseEntity instanceof CourseEntity) {
1119
            return;
1120
        }
1121
1122
        $eventRepo = Container::getCalendarEventRepository();
1123
        $qb = $eventRepo->getResourcesByCourse($courseEntity, $sessionEntity);
1124
1125
        if (!empty($ids)) {
1126
            $qb->andWhere('resource.iid IN (:ids)')
1127
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1128
            ;
1129
        }
1130
1131
        /** @var CCalendarEvent[] $events */
1132
        $events = $qb->getQuery()->getResult();
1133
1134
        /** @var KernelInterface $kernel */
1135
        $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

1135
        /** @scrutinizer ignore-call */ 
1136
        $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...
1136
        $projectDir = rtrim($kernel->getProjectDir(), '/');
1137
        $resourceBase = $projectDir.'/var/upload/resource';
1138
1139
        /** @var ResourceNodeRepository $rnRepo */
1140
        $rnRepo = Container::$container->get(ResourceNodeRepository::class);
1141
1142
        foreach ($events as $ev) {
1143
            $iid = (int) $ev->getIid();
1144
            $title = (string) $ev->getTitle();
1145
            $content = (string) ($ev->getContent() ?? '');
1146
            $startDate = $ev->getStartDate()?->format('Y-m-d H:i:s') ?? '';
1147
            $endDate = $ev->getEndDate()?->format('Y-m-d H:i:s') ?? '';
1148
            $allDay = (int) $ev->isAllDay();
1149
1150
            $firstPath = $firstName = $firstComment = '';
1151
            $firstSize = 0;
1152
1153
            /** @var CCalendarEventAttachment $att */
1154
            foreach ($ev->getAttachments() as $att) {
1155
                $node = $att->getResourceNode();
1156
                $abs = null;
1157
                $size = 0;
1158
                $relForZip = null;
1159
1160
                if ($node) {
1161
                    $file = $node->getFirstResourceFile();
1162
                    if ($file) {
1163
                        $storedRel = (string) $rnRepo->getFilename($file);
1164
                        if ('' !== $storedRel) {
1165
                            $candidate = $resourceBase.$storedRel;
1166
                            if (is_readable($candidate)) {
1167
                                $abs = $candidate;
1168
                                $size = (int) $file->getSize();
1169
                                if ($size <= 0 && is_file($candidate)) {
1170
                                    $st = @stat($candidate);
1171
                                    $size = $st ? (int) $st['size'] : 0;
1172
                                }
1173
                                $base = basename($storedRel) ?: (string) $att->getIid();
1174
                                $relForZip = 'upload/calendar/'.$base;
1175
                            }
1176
                        }
1177
                    }
1178
                }
1179
1180
                if ($abs && $relForZip) {
1181
                    $this->tryAddAsset($relForZip, $abs, $size);
1182
                } else {
1183
                    error_log('COURSE_BUILD: event attachment file not found (event_iid='
1184
                        .$iid.'; att_iid='.(int) $att->getIid().')');
1185
                }
1186
1187
                if ('' === $firstName && $relForZip) {
1188
                    $firstPath = substr($relForZip, \strlen('upload/calendar/'));
1189
                    $firstName = (string) $att->getFilename();
1190
                    $firstComment = (string) ($att->getComment() ?? '');
1191
                    $firstSize = (int) $size;
1192
                }
1193
            }
1194
1195
            $export = new CalendarEvent(
1196
                $iid,
1197
                $title,
1198
                $content,
1199
                $startDate,
1200
                $endDate,
1201
                $firstPath,
1202
                $firstName,
1203
                $firstSize,
1204
                $firstComment,
1205
                $allDay
1206
            );
1207
1208
            $this->course->add_resource($export);
1209
        }
1210
    }
1211
1212
    /**
1213
     * Export Announcements (first attachment legacy, all as assets).
1214
     *
1215
     * @param array<int> $ids
1216
     */
1217
    private function build_announcements(
1218
        object $legacyCourse,
1219
        ?CourseEntity $courseEntity,
1220
        ?SessionEntity $sessionEntity,
1221
        array $ids
1222
    ): void {
1223
        if (!$courseEntity) {
1224
            return;
1225
        }
1226
1227
        $annRepo = Container::getAnnouncementRepository();
1228
        $qb = $annRepo->getResourcesByCourse($courseEntity, $sessionEntity);
1229
1230
        if (!empty($ids)) {
1231
            $qb->andWhere('resource.iid IN (:ids)')
1232
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1233
            ;
1234
        }
1235
1236
        /** @var CAnnouncement[] $anns */
1237
        $anns = $qb->getQuery()->getResult();
1238
1239
        /** @var KernelInterface $kernel */
1240
        $kernel = Container::$container->get('kernel');
1241
        $projectDir = rtrim($kernel->getProjectDir(), '/');
1242
        $resourceBase = $projectDir.'/var/upload/resource';
1243
1244
        /** @var ResourceNodeRepository $rnRepo */
1245
        $rnRepo = Container::$container->get(ResourceNodeRepository::class);
1246
1247
        foreach ($anns as $a) {
1248
            $iid = (int) $a->getIid();
1249
            $title = (string) $a->getTitle();
1250
            $html = (string) ($a->getContent() ?? '');
1251
            $date = $a->getEndDate()?->format('Y-m-d H:i:s') ?? '';
1252
            $email = (bool) $a->getEmailSent();
1253
1254
            $firstPath = $firstName = $firstComment = '';
1255
            $firstSize = 0;
1256
1257
            $attachmentsArr = [];
1258
1259
            /** @var CAnnouncementAttachment $att */
1260
            foreach ($a->getAttachments() as $att) {
1261
                $relPath = ltrim((string) $att->getPath(), '/');
1262
                $assetRel = 'upload/announcements/'.$relPath;
1263
1264
                $abs = null;
1265
                $node = $att->getResourceNode();
1266
                if ($node) {
1267
                    $file = $node->getFirstResourceFile();
1268
                    if ($file) {
1269
                        $storedRel = (string) $rnRepo->getFilename($file);
1270
                        if ('' !== $storedRel) {
1271
                            $candidate = $resourceBase.$storedRel;
1272
                            if (is_readable($candidate)) {
1273
                                $abs = $candidate;
1274
                            }
1275
                        }
1276
                    }
1277
                }
1278
1279
                if ($abs) {
1280
                    $this->tryAddAsset($assetRel, $abs, (int) $att->getSize());
1281
                } else {
1282
                    error_log('COURSE_BUILD: announcement attachment not found (iid='.(int) $att->getIid().')');
1283
                }
1284
1285
                $attachmentsArr[] = [
1286
                    'path' => $relPath,
1287
                    'filename' => (string) $att->getFilename(),
1288
                    'size' => (int) $att->getSize(),
1289
                    'comment' => (string) ($att->getComment() ?? ''),
1290
                    'asset_relpath' => $assetRel,
1291
                ];
1292
1293
                if ('' === $firstName) {
1294
                    $firstPath = $relPath;
1295
                    $firstName = (string) $att->getFilename();
1296
                    $firstSize = (int) $att->getSize();
1297
                    $firstComment = (string) ($att->getComment() ?? '');
1298
                }
1299
            }
1300
1301
            $payload = [
1302
                'title' => $title,
1303
                'content' => $html,
1304
                'date' => $date,
1305
                'display_order' => 0,
1306
                'email_sent' => $email ? 1 : 0,
1307
                'attachment_path' => $firstPath,
1308
                'attachment_filename' => $firstName,
1309
                'attachment_size' => $firstSize,
1310
                'attachment_comment' => $firstComment,
1311
                'attachments' => $attachmentsArr,
1312
            ];
1313
1314
            $legacyCourse->resources[RESOURCE_ANNOUNCEMENT][$iid] =
1315
                $this->mkLegacyItem(RESOURCE_ANNOUNCEMENT, $iid, $payload, ['attachments']);
1316
        }
1317
    }
1318
1319
    /**
1320
     * Register an asset to be packed into the export ZIP.
1321
     *
1322
     * @param string $relPath Relative path inside the ZIP
1323
     * @param string $absPath Absolute filesystem path
1324
     */
1325
    private function addAsset(string $relPath, string $absPath, int $size = 0): void
1326
    {
1327
        if (!isset($this->course->resources['asset']) || !\is_array($this->course->resources['asset'])) {
1328
            $this->course->resources['asset'] = [];
1329
        }
1330
        $this->course->resources['asset'][$relPath] = [
1331
            'abs' => $absPath,
1332
            'size' => $size,
1333
        ];
1334
    }
1335
1336
    /**
1337
     * Try to add an asset only if file exists.
1338
     */
1339
    private function tryAddAsset(string $relPath, string $absPath, int $size = 0): void
1340
    {
1341
        if (is_file($absPath) && is_readable($absPath)) {
1342
            $this->addAsset($relPath, $absPath, $size);
1343
        } else {
1344
            error_log('COURSE_BUILD: asset missing: '.$absPath);
1345
        }
1346
    }
1347
1348
    /**
1349
     * Export Surveys; returns needed Question IDs for follow-up export.
1350
     *
1351
     * @param array<int> $surveyIds
1352
     *
1353
     * @return array<int>
1354
     */
1355
    private function build_surveys(
1356
        object $legacyCourse,
1357
        ?CourseEntity $courseEntity,
1358
        ?SessionEntity $sessionEntity,
1359
        array $surveyIds
1360
    ): array {
1361
        if (!$courseEntity) {
1362
            return [];
1363
        }
1364
1365
        $qb = $this->em->createQueryBuilder()
1366
            ->select('s')
1367
            ->from(CSurvey::class, 's')
1368
            ->innerJoin('s.resourceNode', 'rn')
1369
            ->leftJoin('rn.resourceLinks', 'links')
1370
            ->andWhere('links.course = :course')->setParameter('course', $courseEntity)
1371
            ->andWhere(
1372
                $sessionEntity
1373
                ? '(links.session IS NULL OR links.session = :session)'
1374
                : 'links.session IS NULL'
1375
            )
1376
            ->andWhere('links.deletedAt IS NULL')
1377
            ->andWhere('links.endVisibilityAt IS NULL')
1378
        ;
1379
1380
        if ($sessionEntity) {
1381
            $qb->setParameter('session', $sessionEntity);
1382
        }
1383
        if (!empty($surveyIds)) {
1384
            $qb->andWhere('s.iid IN (:ids)')->setParameter('ids', array_map('intval', $surveyIds));
1385
        }
1386
1387
        /** @var CSurvey[] $surveys */
1388
        $surveys = $qb->getQuery()->getResult();
1389
1390
        $neededQuestionIds = [];
1391
1392
        foreach ($surveys as $s) {
1393
            $iid = (int) $s->getIid();
1394
            $qIds = [];
1395
1396
            foreach ($s->getQuestions() as $q) {
1397
                /** @var CSurveyQuestion $q */
1398
                $qid = (int) $q->getIid();
1399
                $qIds[] = $qid;
1400
                $neededQuestionIds[$qid] = true;
1401
            }
1402
1403
            $payload = [
1404
                'code' => (string) ($s->getCode() ?? ''),
1405
                'title' => (string) $s->getTitle(),
1406
                'subtitle' => (string) ($s->getSubtitle() ?? ''),
1407
                'author' => '',
1408
                'lang' => (string) ($s->getLang() ?? ''),
1409
                'avail_from' => $s->getAvailFrom()?->format('Y-m-d H:i:s'),
1410
                'avail_till' => $s->getAvailTill()?->format('Y-m-d H:i:s'),
1411
                'is_shared' => (string) ($s->getIsShared() ?? '0'),
1412
                'template' => (string) ($s->getTemplate() ?? 'template'),
1413
                'intro' => (string) ($s->getIntro() ?? ''),
1414
                'surveythanks' => (string) ($s->getSurveythanks() ?? ''),
1415
                'creation_date' => $s->getCreationDate()?->format('Y-m-d H:i:s') ?: date('Y-m-d H:i:s'),
1416
                'invited' => (int) $s->getInvited(),
1417
                'answered' => (int) $s->getAnswered(),
1418
                'invite_mail' => (string) $s->getInviteMail(),
1419
                'reminder_mail' => (string) $s->getReminderMail(),
1420
                'mail_subject' => (string) $s->getMailSubject(),
1421
                'anonymous' => (string) $s->getAnonymous(),
1422
                'shuffle' => (bool) $s->getShuffle(),
1423
                'one_question_per_page' => (bool) $s->getOneQuestionPerPage(),
1424
                'visible_results' => $s->getVisibleResults(),
1425
                'display_question_number' => (bool) $s->isDisplayQuestionNumber(),
1426
                'survey_type' => (int) $s->getSurveyType(),
1427
                'show_form_profile' => (int) $s->getShowFormProfile(),
1428
                'form_fields' => (string) $s->getFormFields(),
1429
                'duration' => $s->getDuration(),
1430
                'question_ids' => $qIds,
1431
                'survey_id' => $iid,
1432
            ];
1433
1434
            $legacyCourse->resources[RESOURCE_SURVEY][$iid] =
1435
                $this->mkLegacyItem(RESOURCE_SURVEY, $iid, $payload);
1436
1437
            error_log('COURSE_BUILD: SURVEY iid='.$iid.' qids=['.implode(',', $qIds).']');
1438
        }
1439
1440
        return array_keys($neededQuestionIds);
1441
    }
1442
1443
    /**
1444
     * Export Survey Questions (answers promoted at top level).
1445
     *
1446
     * @param array<int> $questionIds
1447
     */
1448
    private function build_survey_questions(
1449
        object $legacyCourse,
1450
        ?CourseEntity $courseEntity,
1451
        ?SessionEntity $sessionEntity,
1452
        array $questionIds
1453
    ): void {
1454
        if (!$courseEntity) {
1455
            return;
1456
        }
1457
1458
        $qb = $this->em->createQueryBuilder()
1459
            ->select('q', 's')
1460
            ->from(CSurveyQuestion::class, 'q')
1461
            ->innerJoin('q.survey', 's')
1462
            ->innerJoin('s.resourceNode', 'rn')
1463
            ->leftJoin('rn.resourceLinks', 'links')
1464
            ->andWhere('links.course = :course')->setParameter('course', $courseEntity)
1465
            ->andWhere(
1466
                $sessionEntity
1467
                ? '(links.session IS NULL OR links.session = :session)'
1468
                : 'links.session IS NULL'
1469
            )
1470
            ->andWhere('links.deletedAt IS NULL')
1471
            ->andWhere('links.endVisibilityAt IS NULL')
1472
            ->orderBy('s.iid', 'ASC')
1473
            ->addOrderBy('q.sort', 'ASC')
1474
        ;
1475
1476
        if ($sessionEntity) {
1477
            $qb->setParameter('session', $sessionEntity);
1478
        }
1479
        if (!empty($questionIds)) {
1480
            $qb->andWhere('q.iid IN (:ids)')->setParameter('ids', array_map('intval', $questionIds));
1481
        }
1482
1483
        /** @var CSurveyQuestion[] $questions */
1484
        $questions = $qb->getQuery()->getResult();
1485
1486
        $exported = 0;
1487
1488
        foreach ($questions as $q) {
1489
            $qid = (int) $q->getIid();
1490
            $sid = (int) $q->getSurvey()->getIid();
1491
1492
            $answers = [];
1493
            foreach ($q->getOptions() as $opt) {
1494
                /** @var CSurveyQuestionOption $opt */
1495
                $answers[] = [
1496
                    'option_text' => (string) $opt->getOptionText(),
1497
                    'sort' => (int) $opt->getSort(),
1498
                    'value' => (int) $opt->getValue(),
1499
                ];
1500
            }
1501
1502
            $payload = [
1503
                'survey_id' => $sid,
1504
                'survey_question' => (string) $q->getSurveyQuestion(),
1505
                'survey_question_comment' => (string) ($q->getSurveyQuestionComment() ?? ''),
1506
                'type' => (string) $q->getType(),
1507
                'display' => (string) $q->getDisplay(),
1508
                'sort' => (int) $q->getSort(),
1509
                'shared_question_id' => $q->getSharedQuestionId(),
1510
                'max_value' => $q->getMaxValue(),
1511
                'is_required' => (bool) $q->isMandatory(),
1512
                'answers' => $answers,
1513
            ];
1514
1515
            $legacyCourse->resources[RESOURCE_SURVEYQUESTION][$qid] =
1516
                $this->mkLegacyItem(RESOURCE_SURVEYQUESTION, $qid, $payload, ['answers']);
1517
1518
            $exported++;
1519
            error_log('COURSE_BUILD: SURVEY_Q qid='.$qid.' survey='.$sid.' answers='.\count($answers));
1520
        }
1521
1522
        error_log('COURSE_BUILD: survey questions exported='.$exported);
1523
    }
1524
1525
    /**
1526
     * Export Quizzes and return required Question IDs.
1527
     *
1528
     * @param array<int> $quizIds
1529
     *
1530
     * @return array<int>
1531
     */
1532
    private function build_quizzes(
1533
        object $legacyCourse,
1534
        ?CourseEntity $courseEntity,
1535
        ?SessionEntity $sessionEntity,
1536
        array $quizIds
1537
    ): array {
1538
        if (!$courseEntity) {
1539
            return [];
1540
        }
1541
1542
        $qb = $this->em->createQueryBuilder()
1543
            ->select('q')
1544
            ->from(CQuiz::class, 'q')
1545
            ->innerJoin('q.resourceNode', 'rn')
1546
            ->leftJoin('rn.resourceLinks', 'links')
1547
            ->andWhere('links.course = :course')->setParameter('course', $courseEntity)
1548
            ->andWhere(
1549
                $sessionEntity
1550
                ? '(links.session IS NULL OR links.session = :session)'
1551
                : 'links.session IS NULL'
1552
            )
1553
            ->andWhere('links.deletedAt IS NULL')
1554
            ->andWhere('links.endVisibilityAt IS NULL')
1555
        ;
1556
1557
        if ($sessionEntity) {
1558
            $qb->setParameter('session', $sessionEntity);
1559
        }
1560
        if (!empty($quizIds)) {
1561
            $qb->andWhere('q.iid IN (:ids)')->setParameter('ids', array_map('intval', $quizIds));
1562
        }
1563
1564
        /** @var CQuiz[] $quizzes */
1565
        $quizzes = $qb->getQuery()->getResult();
1566
        $neededQuestionIds = [];
1567
1568
        foreach ($quizzes as $quiz) {
1569
            $iid = (int) $quiz->getIid();
1570
1571
            $payload = [
1572
                'title' => (string) $quiz->getTitle(),
1573
                'description' => (string) ($quiz->getDescription() ?? ''),
1574
                'type' => (int) $quiz->getType(),
1575
                'random' => (int) $quiz->getRandom(),
1576
                'random_answers' => (bool) $quiz->getRandomAnswers(),
1577
                'results_disabled' => (int) $quiz->getResultsDisabled(),
1578
                'max_attempt' => (int) $quiz->getMaxAttempt(),
1579
                'feedback_type' => (int) $quiz->getFeedbackType(),
1580
                'expired_time' => (int) $quiz->getExpiredTime(),
1581
                'review_answers' => (int) $quiz->getReviewAnswers(),
1582
                'random_by_category' => (int) $quiz->getRandomByCategory(),
1583
                'text_when_finished' => (string) ($quiz->getTextWhenFinished() ?? ''),
1584
                'text_when_finished_failure' => (string) ($quiz->getTextWhenFinishedFailure() ?? ''),
1585
                'display_category_name' => (int) $quiz->getDisplayCategoryName(),
1586
                'save_correct_answers' => (int) ($quiz->getSaveCorrectAnswers() ?? 0),
1587
                'propagate_neg' => (int) $quiz->getPropagateNeg(),
1588
                'hide_question_title' => (bool) $quiz->isHideQuestionTitle(),
1589
                'hide_question_number' => (int) $quiz->getHideQuestionNumber(),
1590
                'question_selection_type' => (int) ($quiz->getQuestionSelectionType() ?? 0),
1591
                'access_condition' => (string) ($quiz->getAccessCondition() ?? ''),
1592
                'pass_percentage' => $quiz->getPassPercentage(),
1593
                'start_time' => $quiz->getStartTime()?->format('Y-m-d H:i:s'),
1594
                'end_time' => $quiz->getEndTime()?->format('Y-m-d H:i:s'),
1595
                'question_ids' => [],
1596
                'question_orders' => [],
1597
            ];
1598
1599
            $rels = $this->em->createQueryBuilder()
1600
                ->select('rel', 'qq')
1601
                ->from(CQuizRelQuestion::class, 'rel')
1602
                ->innerJoin('rel.question', 'qq')
1603
                ->andWhere('rel.quiz = :quiz')
1604
                ->setParameter('quiz', $quiz)
1605
                ->orderBy('rel.questionOrder', 'ASC')
1606
                ->getQuery()->getResult()
1607
            ;
1608
1609
            foreach ($rels as $rel) {
1610
                $qid = (int) $rel->getQuestion()->getIid();
1611
                $payload['question_ids'][] = $qid;
1612
                $payload['question_orders'][] = (int) $rel->getQuestionOrder();
1613
                $neededQuestionIds[$qid] = true;
1614
            }
1615
1616
            $legacyCourse->resources[RESOURCE_QUIZ][$iid] =
1617
                $this->mkLegacyItem(
1618
                    RESOURCE_QUIZ,
1619
                    $iid,
1620
                    $payload,
1621
                    ['question_ids', 'question_orders']
1622
                );
1623
        }
1624
1625
        error_log(
1626
            'COURSE_BUILD: build_quizzes done; total='.\count($quizzes)
1627
        );
1628
1629
        return array_keys($neededQuestionIds);
1630
    }
1631
1632
    /**
1633
     * Safe count helper for mixed values.
1634
     */
1635
    private function safeCount(mixed $v): int
1636
    {
1637
        return (\is_array($v) || $v instanceof Countable) ? \count($v) : 0;
1638
    }
1639
1640
    /**
1641
     * Export Quiz Questions (answers and options promoted).
1642
     *
1643
     * @param array<int> $questionIds
1644
     */
1645
    private function build_quiz_questions(
1646
        object $legacyCourse,
1647
        ?CourseEntity $courseEntity,
1648
        ?SessionEntity $sessionEntity,
1649
        array $questionIds
1650
    ): void {
1651
        if (!$courseEntity) {
1652
            return;
1653
        }
1654
1655
        error_log('COURSE_BUILD: build_quiz_questions start ids='.json_encode(array_values($questionIds)));
1656
        error_log('COURSE_BUILD: build_quiz_questions exported='.$this->safeCount($legacyCourse->resources[RESOURCE_QUIZQUESTION] ?? 0));
1657
1658
        $qb = $this->em->createQueryBuilder()
1659
            ->select('qq')
1660
            ->from(CQuizQuestion::class, 'qq')
1661
            ->innerJoin('qq.resourceNode', 'qrn')
1662
            ->leftJoin('qrn.resourceLinks', 'qlinks')
1663
            ->andWhere('qlinks.course = :course')->setParameter('course', $courseEntity)
1664
            ->andWhere(
1665
                $sessionEntity
1666
                ? '(qlinks.session IS NULL OR qlinks.session = :session)'
1667
                : 'qlinks.session IS NULL'
1668
            )
1669
            ->andWhere('qlinks.deletedAt IS NULL')
1670
            ->andWhere('qlinks.endVisibilityAt IS NULL')
1671
        ;
1672
1673
        if ($sessionEntity) {
1674
            $qb->setParameter('session', $sessionEntity);
1675
        }
1676
        if (!empty($questionIds)) {
1677
            $qb->andWhere('qq.iid IN (:ids)')->setParameter('ids', array_map('intval', $questionIds));
1678
        }
1679
1680
        /** @var CQuizQuestion[] $questions */
1681
        $questions = $qb->getQuery()->getResult();
1682
1683
        error_log('COURSE_BUILD: build_quiz_questions start ids='.json_encode(array_values($questionIds)));
1684
        error_log('COURSE_BUILD: build_quiz_questions exported='.$this->safeCount($legacyCourse->resources[RESOURCE_QUIZQUESTION] ?? 0));
1685
1686
        $this->exportQuestionsWithAnswers($legacyCourse, $questions);
1687
    }
1688
1689
    /**
1690
     * Internal exporter for quiz questions + answers (+ options for MATF type).
1691
     *
1692
     * @param array<int,CQuizQuestion> $questions
1693
     */
1694
    private function exportQuestionsWithAnswers(object $legacyCourse, array $questions): void
1695
    {
1696
        foreach ($questions as $q) {
1697
            $qid = (int) $q->getIid();
1698
1699
            $payload = [
1700
                'question' => (string) $q->getQuestion(),
1701
                'description' => (string) ($q->getDescription() ?? ''),
1702
                'ponderation' => (float) $q->getPonderation(),
1703
                'position' => (int) $q->getPosition(),
1704
                'type' => (int) $q->getType(),
1705
                'quiz_type' => (int) $q->getType(),
1706
                'picture' => (string) ($q->getPicture() ?? ''),
1707
                'level' => (int) $q->getLevel(),
1708
                'extra' => (string) ($q->getExtra() ?? ''),
1709
                'feedback' => (string) ($q->getFeedback() ?? ''),
1710
                'question_code' => (string) ($q->getQuestionCode() ?? ''),
1711
                'mandatory' => (int) $q->getMandatory(),
1712
                'duration' => $q->getDuration(),
1713
                'parent_media_id' => $q->getParentMediaId(),
1714
                'answers' => [],
1715
            ];
1716
1717
            $ans = $this->em->createQueryBuilder()
1718
                ->select('a')
1719
                ->from(CQuizAnswer::class, 'a')
1720
                ->andWhere('a.question = :q')->setParameter('q', $q)
1721
                ->orderBy('a.position', 'ASC')
1722
                ->getQuery()->getResult()
1723
            ;
1724
1725
            foreach ($ans as $a) {
1726
                $payload['answers'][] = [
1727
                    'id' => (int) $a->getIid(),
1728
                    'answer' => (string) $a->getAnswer(),
1729
                    'comment' => (string) ($a->getComment() ?? ''),
1730
                    'ponderation' => (float) $a->getPonderation(),
1731
                    'position' => (int) $a->getPosition(),
1732
                    'hotspot_coordinates' => $a->getHotspotCoordinates(),
1733
                    'hotspot_type' => $a->getHotspotType(),
1734
                    'correct' => $a->getCorrect(),
1735
                ];
1736
            }
1737
1738
            if (\defined('MULTIPLE_ANSWER_TRUE_FALSE') && MULTIPLE_ANSWER_TRUE_FALSE === (int) $q->getType()) {
1739
                $opts = $this->em->createQueryBuilder()
1740
                    ->select('o')
1741
                    ->from(CQuizQuestionOption::class, 'o')
1742
                    ->andWhere('o.question = :q')->setParameter('q', $q)
1743
                    ->orderBy('o.position', 'ASC')
1744
                    ->getQuery()->getResult()
1745
                ;
1746
1747
                $payload['question_options'] = array_map(static fn ($o) => [
1748
                    'id' => (int) $o->getIid(),
1749
                    'name' => (string) $o->getTitle(),
1750
                    'position' => (int) $o->getPosition(),
1751
                ], $opts);
1752
            }
1753
1754
            $legacyCourse->resources[RESOURCE_QUIZQUESTION][$qid] =
1755
                $this->mkLegacyItem(RESOURCE_QUIZQUESTION, $qid, $payload, ['answers', 'question_options']);
1756
1757
            error_log(
1758
                'COURSE_BUILD: QQ qid='.$qid.
1759
                ' quiz_type='.($legacyCourse->resources[RESOURCE_QUIZQUESTION][$qid]->quiz_type ?? 'missing').
1760
                ' answers='.\count($legacyCourse->resources[RESOURCE_QUIZQUESTION][$qid]->answers ?? [])
1761
            );
1762
        }
1763
    }
1764
1765
    /**
1766
     * Export Link category as legacy item.
1767
     */
1768
    private function build_link_category(CLinkCategory $category): void
1769
    {
1770
        $id = (int) $category->getIid();
1771
        if ($id <= 0) {
1772
            return;
1773
        }
1774
1775
        $payload = [
1776
            'title' => (string) $category->getTitle(),
1777
            'description' => (string) ($category->getDescription() ?? ''),
1778
            'category_title' => (string) $category->getTitle(),
1779
        ];
1780
1781
        $this->course->resources[RESOURCE_LINKCATEGORY][$id] =
1782
            $this->mkLegacyItem(RESOURCE_LINKCATEGORY, $id, $payload);
1783
    }
1784
1785
    /**
1786
     * Export Links (and their categories once).
1787
     *
1788
     * @param array<int> $ids
1789
     */
1790
    private function build_links(
1791
        object $legacyCourse,
1792
        ?CourseEntity $courseEntity,
1793
        ?SessionEntity $sessionEntity,
1794
        array $ids
1795
    ): void {
1796
        if (!$courseEntity instanceof CourseEntity) {
1797
            return;
1798
        }
1799
1800
        $linkRepo = Container::getLinkRepository();
1801
        $catRepo = Container::getLinkCategoryRepository();
1802
1803
        $qb = $linkRepo->getResourcesByCourse($courseEntity, $sessionEntity);
1804
1805
        if (!empty($ids)) {
1806
            $qb->andWhere('resource.iid IN (:ids)')
1807
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1808
            ;
1809
        }
1810
1811
        /** @var CLink[] $links */
1812
        $links = $qb->getQuery()->getResult();
1813
1814
        $exportedCats = [];
1815
1816
        foreach ($links as $link) {
1817
            $iid = (int) $link->getIid();
1818
            $title = (string) $link->getTitle();
1819
            $url = (string) $link->getUrl();
1820
            $desc = (string) ($link->getDescription() ?? '');
1821
            $tgt = (string) ($link->getTarget() ?? '');
1822
1823
            $cat = $link->getCategory();
1824
            $catId = (int) ($cat?->getIid() ?? 0);
1825
1826
            if ($catId > 0 && !isset($exportedCats[$catId])) {
1827
                $this->build_link_category($cat);
1828
                $exportedCats[$catId] = true;
1829
            }
1830
1831
            $payload = [
1832
                'title' => '' !== $title ? $title : $url,
1833
                'url' => $url,
1834
                'description' => $desc,
1835
                'target' => $tgt,
1836
                'category_id' => $catId,
1837
                'on_homepage' => false,
1838
            ];
1839
1840
            $legacyCourse->resources[RESOURCE_LINK][$iid] =
1841
                $this->mkLegacyItem(RESOURCE_LINK, $iid, $payload);
1842
        }
1843
    }
1844
1845
    /**
1846
     * Format DateTime as string "Y-m-d H:i:s".
1847
     */
1848
    private function fmtDate(?DateTimeInterface $dt): string
1849
    {
1850
        return $dt ? $dt->format('Y-m-d H:i:s') : '';
1851
    }
1852
1853
    /**
1854
     * Create a legacy item object, promoting selected array keys to top-level.
1855
     *
1856
     * @param array<int,string> $arrayKeysToPromote
1857
     */
1858
    private function mkLegacyItem(string $type, int $sourceId, array|object $obj, array $arrayKeysToPromote = []): stdClass
1859
    {
1860
        $o = new stdClass();
1861
        $o->type = $type;
1862
        $o->source_id = $sourceId;
1863
        $o->destination_id = -1;
1864
        $o->has_obj = true;
1865
        $o->obj = (object) $obj;
1866
1867
        if (!isset($o->obj->iid)) {
1868
            $o->obj->iid = $sourceId;
1869
        }
1870
        if (!isset($o->id)) {
1871
            $o->id = $sourceId;
1872
        }
1873
        if (!isset($o->obj->id)) {
1874
            $o->obj->id = $sourceId;
1875
        }
1876
1877
        foreach ((array) $obj as $k => $v) {
1878
            if (\is_scalar($v) || null === $v) {
1879
                if (!property_exists($o, $k)) {
1880
                    $o->{$k} = $v;
1881
                }
1882
            }
1883
        }
1884
        foreach ($arrayKeysToPromote as $k) {
1885
            if (isset($obj[$k]) && \is_array($obj[$k])) {
1886
                $o->{$k} = $obj[$k];
1887
            }
1888
        }
1889
1890
        if (RESOURCE_DOCUMENT === $type) {
1891
            $o->path = (string) ($o->path ?? $o->full_path ?? $o->obj->path ?? $o->obj->full_path ?? '');
1892
            $o->full_path = (string) ($o->full_path ?? $o->path ?? $o->obj->full_path ?? $o->obj->path ?? '');
1893
            $o->file_type = (string) ($o->file_type ?? $o->filetype ?? $o->obj->file_type ?? $o->obj->filetype ?? '');
1894
            $o->filetype = (string) ($o->filetype ?? $o->file_type ?? $o->obj->filetype ?? $o->obj->file_type ?? '');
1895
            $o->title = (string) ($o->title ?? $o->obj->title ?? '');
1896
            if (!isset($o->name) || '' === $o->name || null === $o->name) {
1897
                $o->name = '' !== $o->title ? $o->title : ('document '.$sourceId);
1898
            }
1899
        }
1900
1901
        if (RESOURCE_SURVEYQUESTION === $type) {
1902
            if (!isset($o->survey_question_type) && isset($o->type)) {
1903
                $o->survey_question_type = $o->type;
1904
            }
1905
            if (!isset($o->type) && isset($o->survey_question_type)) {
1906
                $o->type = $o->survey_question_type;
1907
            }
1908
1909
            if (isset($o->obj) && \is_object($o->obj)) {
1910
                if (!isset($o->obj->survey_question_type) && isset($o->obj->type)) {
1911
                    $o->obj->survey_question_type = $o->obj->type;
1912
                }
1913
                if (!isset($o->obj->type) && isset($o->obj->survey_question_type)) {
1914
                    $o->obj->type = $o->obj->survey_question_type;
1915
                }
1916
            }
1917
        }
1918
1919
        if (!isset($o->name) || '' === $o->name || null === $o->name) {
1920
            if (isset($obj['name']) && '' !== $obj['name']) {
1921
                $o->name = (string) $obj['name'];
1922
            } elseif (isset($obj['title']) && '' !== $obj['title']) {
1923
                $o->name = (string) $obj['title'];
1924
            } else {
1925
                $o->name = $type.' '.$sourceId;
1926
            }
1927
        }
1928
1929
        return $o;
1930
    }
1931
1932
    /**
1933
     * Build an id filter closure.
1934
     *
1935
     * @param array<int> $idsFilter
1936
     *
1937
     * @return Closure(int):bool
1938
     */
1939
    private function makeIdFilter(array $idsFilter): Closure
1940
    {
1941
        if (empty($idsFilter)) {
1942
            return static fn (int $id): bool => true;
1943
        }
1944
        $set = array_fill_keys(array_map('intval', $idsFilter), true);
1945
1946
        return static fn (int $id): bool => isset($set[$id]);
1947
    }
1948
1949
    /**
1950
     * Export Tool intro (tool -> intro text) for visible tools.
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
        $qb = $repo->createQueryBuilder('ti')
1964
            ->innerJoin('ti.courseTool', 'ct')
1965
            ->andWhere('ct.course = :course')
1966
            ->setParameter('course', $courseEntity)
1967
        ;
1968
1969
        if ($sessionEntity) {
1970
            $qb->andWhere('ct.session = :session')->setParameter('session', $sessionEntity);
1971
        } else {
1972
            $qb->andWhere('ct.session IS NULL');
1973
        }
1974
1975
        $qb->andWhere('ct.visibility = :vis')->setParameter('vis', true);
1976
1977
        /** @var CToolIntro[] $intros */
1978
        $intros = $qb->getQuery()->getResult();
1979
1980
        foreach ($intros as $intro) {
1981
            $ctool = $intro->getCourseTool();       // CTool
1982
            $titleKey = (string) $ctool->getTitle();   // e.g. 'documents', 'forum'
1983
            if ('' === $titleKey) {
1984
                continue;
1985
            }
1986
1987
            $payload = [
1988
                'id' => $titleKey,
1989
                'intro_text' => (string) $intro->getIntroText(),
1990
            ];
1991
1992
            // Use 0 as source_id (unused by restore)
1993
            $legacyCourse->resources[RESOURCE_TOOL_INTRO][$titleKey] =
1994
                $this->mkLegacyItem(RESOURCE_TOOL_INTRO, 0, $payload);
1995
        }
1996
    }
1997
1998
    /**
1999
     * Export Forum categories.
2000
     *
2001
     * @param array<int> $ids
2002
     */
2003
    private function build_forum_category(
2004
        object $legacyCourse,
2005
        CourseEntity $courseEntity,
2006
        ?SessionEntity $sessionEntity,
2007
        array $ids
2008
    ): void {
2009
        $repo = Container::getForumCategoryRepository();
2010
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
2011
        $categories = $qb->getQuery()->getResult();
2012
2013
        $keep = $this->makeIdFilter($ids);
2014
2015
        foreach ($categories as $cat) {
2016
            /** @var CForumCategory $cat */
2017
            $id = (int) $cat->getIid();
2018
            if (!$keep($id)) {
2019
                continue;
2020
            }
2021
2022
            $payload = [
2023
                'title' => (string) $cat->getTitle(),
2024
                'description' => (string) ($cat->getCatComment() ?? ''),
2025
                'cat_title' => (string) $cat->getTitle(),
2026
                'cat_comment' => (string) ($cat->getCatComment() ?? ''),
2027
            ];
2028
2029
            $legacyCourse->resources[RESOURCE_FORUMCATEGORY][$id] =
2030
                $this->mkLegacyItem(RESOURCE_FORUMCATEGORY, $id, $payload);
2031
        }
2032
    }
2033
2034
    /**
2035
     * Export Forums.
2036
     *
2037
     * @param array<int> $ids
2038
     */
2039
    private function build_forums(
2040
        object $legacyCourse,
2041
        CourseEntity $courseEntity,
2042
        ?SessionEntity $sessionEntity,
2043
        array $ids
2044
    ): void {
2045
        $repo = Container::getForumRepository();
2046
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
2047
        $forums = $qb->getQuery()->getResult();
2048
2049
        $keep = $this->makeIdFilter($ids);
2050
2051
        foreach ($forums as $f) {
2052
            /** @var CForum $f */
2053
            $id = (int) $f->getIid();
2054
            if (!$keep($id)) {
2055
                continue;
2056
            }
2057
2058
            $payload = [
2059
                'title' => (string) $f->getTitle(),
2060
                'description' => (string) ($f->getForumComment() ?? ''),
2061
                'forum_title' => (string) $f->getTitle(),
2062
                'forum_comment' => (string) ($f->getForumComment() ?? ''),
2063
                'forum_category' => (int) ($f->getForumCategory()?->getIid() ?? 0),
2064
                'allow_anonymous' => (int) ($f->getAllowAnonymous() ?? 0),
2065
                'allow_edit' => (int) ($f->getAllowEdit() ?? 0),
2066
                'approval_direct_post' => (string) ($f->getApprovalDirectPost() ?? '0'),
2067
                'allow_attachments' => (int) ($f->getAllowAttachments() ?? 1),
2068
                'allow_new_threads' => (int) ($f->getAllowNewThreads() ?? 1),
2069
                'default_view' => (string) ($f->getDefaultView() ?? 'flat'),
2070
                'forum_of_group' => (string) ($f->getForumOfGroup() ?? '0'),
2071
                'forum_group_public_private' => (string) ($f->getForumGroupPublicPrivate() ?? 'public'),
2072
                'moderated' => (int) ($f->isModerated() ? 1 : 0),
2073
                'start_time' => $this->fmtDate($f->getStartTime()),
2074
                'end_time' => $this->fmtDate($f->getEndTime()),
2075
            ];
2076
2077
            $legacyCourse->resources[RESOURCE_FORUM][$id] =
2078
                $this->mkLegacyItem(RESOURCE_FORUM, $id, $payload);
2079
        }
2080
    }
2081
2082
    /**
2083
     * Export Forum threads.
2084
     *
2085
     * @param array<int> $ids
2086
     */
2087
    private function build_forum_topics(
2088
        object $legacyCourse,
2089
        CourseEntity $courseEntity,
2090
        ?SessionEntity $sessionEntity,
2091
        array $ids
2092
    ): void {
2093
        $repo = Container::getForumThreadRepository();
2094
        $qb = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
2095
        $threads = $qb->getQuery()->getResult();
2096
2097
        $keep = $this->makeIdFilter($ids);
2098
2099
        foreach ($threads as $t) {
2100
            /** @var CForumThread $t */
2101
            $id = (int) $t->getIid();
2102
            if (!$keep($id)) {
2103
                continue;
2104
            }
2105
2106
            $payload = [
2107
                'title' => (string) $t->getTitle(),
2108
                'thread_title' => (string) $t->getTitle(),
2109
                'title_qualify' => (string) ($t->getThreadTitleQualify() ?? ''),
2110
                'topic_poster_name' => (string) ($t->getUser()?->getUsername() ?? ''),
2111
                'forum_id' => (int) ($t->getForum()?->getIid() ?? 0),
2112
                'thread_date' => $this->fmtDate($t->getThreadDate()),
2113
                'thread_sticky' => (int) ($t->getThreadSticky() ? 1 : 0),
2114
                'thread_title_qualify' => (string) ($t->getThreadTitleQualify() ?? ''),
2115
                'thread_qualify_max' => (float) $t->getThreadQualifyMax(),
2116
                'thread_weight' => (float) $t->getThreadWeight(),
2117
                'thread_peer_qualify' => (int) ($t->isThreadPeerQualify() ? 1 : 0),
2118
            ];
2119
2120
            $legacyCourse->resources[RESOURCE_FORUMTOPIC][$id] =
2121
                $this->mkLegacyItem(RESOURCE_FORUMTOPIC, $id, $payload);
2122
        }
2123
    }
2124
2125
    /**
2126
     * Export first post for each thread as topic root post.
2127
     *
2128
     * @param array<int> $ids
2129
     */
2130
    private function build_forum_posts(
2131
        object $legacyCourse,
2132
        CourseEntity $courseEntity,
2133
        ?SessionEntity $sessionEntity,
2134
        array $ids
2135
    ): void {
2136
        $repoThread = Container::getForumThreadRepository();
2137
        $repoPost = Container::getForumPostRepository();
2138
2139
        $qb = $repoThread->getResourcesByCourse($courseEntity, $sessionEntity);
2140
        $threads = $qb->getQuery()->getResult();
2141
2142
        $keep = $this->makeIdFilter($ids);
2143
2144
        foreach ($threads as $t) {
2145
            /** @var CForumThread $t */
2146
            $threadId = (int) $t->getIid();
2147
            if (!$keep($threadId)) {
2148
                continue;
2149
            }
2150
2151
            $first = $repoPost->findOneBy(['thread' => $t], ['postDate' => 'ASC', 'iid' => 'ASC']);
2152
            if (!$first) {
2153
                continue;
2154
            }
2155
2156
            $postId = (int) $first->getIid();
2157
            $titleFromPost = trim((string) $first->getTitle());
2158
            if ('' === $titleFromPost) {
2159
                $plain = trim(strip_tags((string) ($first->getPostText() ?? '')));
2160
                $titleFromPost = mb_substr('' !== $plain ? $plain : 'Post', 0, 60);
2161
            }
2162
2163
            $payload = [
2164
                'title' => $titleFromPost,
2165
                'post_title' => $titleFromPost,
2166
                'post_text' => (string) ($first->getPostText() ?? ''),
2167
                'thread_id' => $threadId,
2168
                'forum_id' => (int) ($t->getForum()?->getIid() ?? 0),
2169
                'post_notification' => (int) ($first->getPostNotification() ? 1 : 0),
2170
                'visible' => (int) ($first->getVisible() ? 1 : 0),
2171
                'status' => (int) ($first->getStatus() ?? CForumPost::STATUS_VALIDATED),
2172
                'post_parent_id' => (int) ($first->getPostParent()?->getIid() ?? 0),
2173
                'poster_id' => (int) ($first->getUser()?->getId() ?? 0),
2174
                'text' => (string) ($first->getPostText() ?? ''),
2175
                'poster_name' => (string) ($first->getUser()?->getUsername() ?? ''),
2176
                'post_date' => $this->fmtDate($first->getPostDate()),
2177
            ];
2178
2179
            $legacyCourse->resources[RESOURCE_FORUMPOST][$postId] =
2180
                $this->mkLegacyItem(RESOURCE_FORUMPOST, $postId, $payload);
2181
        }
2182
    }
2183
2184
    /**
2185
     * New Chamilo 2 build: CDocumentRepository-based (instead of legacy tables).
2186
     *
2187
     * @param array<int> $idList
2188
     */
2189
    private function build_documents_with_repo(
2190
        ?CourseEntity $course,
2191
        ?SessionEntity $session,
2192
        bool $withBaseContent,
2193
        array $idList = []
2194
    ): void {
2195
        if (!$course instanceof CourseEntity) {
2196
            return;
2197
        }
2198
2199
        $qb = $this->docRepo->getResourcesByCourse(
2200
            $course,
2201
            $session,
2202
            null,
2203
            null,
2204
            true,
2205
            false
2206
        );
2207
2208
        if (!empty($idList)) {
2209
            $qb->andWhere('resource.iid IN (:ids)')
2210
                ->setParameter('ids', array_values(array_unique(array_map('intval', $idList))))
2211
            ;
2212
        }
2213
2214
        /** @var CDocument[] $docs */
2215
        $docs = $qb->getQuery()->getResult();
2216
2217
        foreach ($docs as $doc) {
2218
            $node = $doc->getResourceNode();
2219
            $filetype = $doc->getFiletype(); // 'file'|'folder'|...
2220
            $title = $doc->getTitle();
2221
            $comment = $doc->getComment() ?? '';
2222
            $iid = (int) $doc->getIid();
2223
            $fullPath = $doc->getFullPath();
2224
2225
            // Determine size
2226
            $size = 0;
2227
            if ('folder' === $filetype) {
2228
                $size = $this->docRepo->getFolderSize($node, $course, $session);
2229
            } else {
2230
                /** @var Collection<int,ResourceFile>|null $files */
2231
                $files = $node?->getResourceFiles();
2232
                if ($files && $files->count() > 0) {
2233
                    /** @var ResourceFile $first */
2234
                    $first = $files->first();
2235
                    $size = (int) $first->getSize();
2236
                }
2237
            }
2238
2239
            $exportDoc = new Document(
2240
                $iid,
2241
                '/'.$fullPath,
2242
                $comment,
2243
                $title,
2244
                $filetype,
2245
                (string) $size
2246
            );
2247
2248
            $this->course->add_resource($exportDoc);
2249
        }
2250
    }
2251
2252
    /**
2253
     * Backward-compatible wrapper for build_documents_with_repo().
2254
     *
2255
     * @param array<int> $idList
2256
     */
2257
    public function build_documents(
2258
        int $session_id = 0,
2259
        int $courseId = 0,
2260
        bool $withBaseContent = false,
2261
        array $idList = []
2262
    ): void {
2263
        /** @var CourseEntity|null $course */
2264
        $course = $this->em->getRepository(CourseEntity::class)->find($courseId);
2265
2266
        /** @var SessionEntity|null $session */
2267
        $session = $session_id ? $this->em->getRepository(SessionEntity::class)->find($session_id) : null;
2268
2269
        $this->build_documents_with_repo($course, $session, $withBaseContent, $idList);
2270
    }
2271
}
2272