Passed
Pull Request — master (#7027)
by
unknown
09:12
created

CourseBuilder::build_links()   B

Complexity

Conditions 7
Paths 11

Size

Total Lines 52
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 30
c 0
b 0
f 0
nc 11
nop 4
dl 0
loc 52
rs 8.5066

How to fix   Long Method   

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

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

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

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

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