CourseBuilder::build_thematic()   B
last analyzed

Complexity

Conditions 9
Paths 35

Size

Total Lines 84
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 50
c 0
b 0
f 0
nc 35
nop 4
dl 0
loc 84
rs 7.5353

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\ResourceLink;
15
use Chamilo\CoreBundle\Entity\ResourceNode;
16
use Chamilo\CoreBundle\Entity\Session as SessionEntity;
17
use Chamilo\CoreBundle\Framework\Container;
18
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
19
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Attendance;
20
use Chamilo\CourseBundle\Component\CourseCopy\Resources\CalendarEvent;
21
use Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseDescription;
22
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Document;
23
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Glossary;
24
use Chamilo\CourseBundle\Component\CourseCopy\Resources\GradeBookBackup;
25
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Thematic;
26
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Wiki;
27
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Work;
28
use Chamilo\CourseBundle\Entity\CAnnouncement;
29
use Chamilo\CourseBundle\Entity\CAnnouncementAttachment;
30
use Chamilo\CourseBundle\Entity\CAttendance;
31
use Chamilo\CourseBundle\Entity\CAttendanceCalendar;
32
use Chamilo\CourseBundle\Entity\CCalendarEvent;
33
use Chamilo\CourseBundle\Entity\CCalendarEventAttachment;
34
use Chamilo\CourseBundle\Entity\CCourseDescription;
35
use Chamilo\CourseBundle\Entity\CDocument;
36
use Chamilo\CourseBundle\Entity\CForum;
37
use Chamilo\CourseBundle\Entity\CForumCategory;
38
use Chamilo\CourseBundle\Entity\CForumPost;
39
use Chamilo\CourseBundle\Entity\CForumThread;
40
use Chamilo\CourseBundle\Entity\CGlossary;
41
use Chamilo\CourseBundle\Entity\CLink;
42
use Chamilo\CourseBundle\Entity\CLinkCategory;
43
use Chamilo\CourseBundle\Entity\CLp;
44
use Chamilo\CourseBundle\Entity\CLpCategory;
45
use Chamilo\CourseBundle\Entity\CLpItem;
46
use Chamilo\CourseBundle\Entity\CQuiz;
47
use Chamilo\CourseBundle\Entity\CQuizAnswer;
48
use Chamilo\CourseBundle\Entity\CQuizQuestion;
49
use Chamilo\CourseBundle\Entity\CQuizQuestionOption;
50
use Chamilo\CourseBundle\Entity\CQuizRelQuestion;
51
use Chamilo\CourseBundle\Entity\CStudentPublication;
52
use Chamilo\CourseBundle\Entity\CSurvey;
53
use Chamilo\CourseBundle\Entity\CSurveyQuestion;
54
use Chamilo\CourseBundle\Entity\CSurveyQuestionOption;
55
use Chamilo\CourseBundle\Entity\CThematic;
56
use Chamilo\CourseBundle\Entity\CThematicAdvance;
57
use Chamilo\CourseBundle\Entity\CThematicPlan;
58
use Chamilo\CourseBundle\Entity\CTool;
59
use Chamilo\CourseBundle\Entity\CToolIntro;
60
use Chamilo\CourseBundle\Entity\CWiki;
61
use Chamilo\CourseBundle\Repository\CDocumentRepository;
62
use Closure;
63
use Countable;
64
use Database;
65
use DateTimeInterface;
66
use Doctrine\Common\Collections\Collection;
67
use Doctrine\ORM\EntityManagerInterface;
68
use Doctrine\ORM\QueryBuilder;
69
use DocumentManager;
70
use ReflectionProperty;
71
use stdClass;
72
use Symfony\Component\HttpKernel\KernelInterface;
73
use Throwable;
74
75
use const PHP_URL_PATH;
76
77
/**
78
 * CourseBuilder focused on Doctrine/ResourceNode export (keeps legacy orchestration).
79
 */
80
class CourseBuilder
81
{
82
    /**
83
     * @var Course Legacy course container used by the exporter
84
     */
85
    public $course;
86
87
    /**
88
     * @var array<string> Only the tools to build (defaults kept)
89
     */
90
    public array $tools_to_build = [
91
        'documents', 'forums', 'tool_intro', 'links', 'quizzes', 'quiz_questions',
92
        'assets', 'surveys', 'survey_questions', 'announcements', 'events',
93
        'course_descriptions', 'glossary', 'wiki', 'thematic', 'attendance', 'works',
94
        'gradebook', 'learnpath_category', 'learnpaths',
95
    ];
96
97
    /**
98
     * @var array<string, int|string> Legacy constant map (extend as you add tools)
99
     */
100
    public array $toolToName = [
101
        'documents' => RESOURCE_DOCUMENT,
102
        'forums' => RESOURCE_FORUM,
103
        'tool_intro' => RESOURCE_TOOL_INTRO,
104
        'links' => RESOURCE_LINK,
105
        'quizzes' => RESOURCE_QUIZ,
106
        'quiz_questions' => RESOURCE_QUIZQUESTION,
107
        'assets' => 'asset',
108
        'surveys' => RESOURCE_SURVEY,
109
        'survey_questions' => RESOURCE_SURVEYQUESTION,
110
        'announcements' => RESOURCE_ANNOUNCEMENT,
111
        'events' => RESOURCE_EVENT,
112
        'course_descriptions' => RESOURCE_COURSEDESCRIPTION,
113
        'glossary' => RESOURCE_GLOSSARY,
114
        'wiki' => RESOURCE_WIKI,
115
        'thematic' => RESOURCE_THEMATIC,
116
        'attendance' => RESOURCE_ATTENDANCE,
117
        'works' => RESOURCE_WORK,
118
        'gradebook' => RESOURCE_GRADEBOOK,
119
        'learnpaths' => RESOURCE_LEARNPATH,
120
        'learnpath_category' => RESOURCE_LEARNPATH_CATEGORY,
121
    ];
122
123
    /**
124
     * @var array<string, array<int>> Optional whitelist of IDs per tool
125
     */
126
    public array $specific_id_list = [];
127
128
    /**
129
     * Documents referenced inside HTML.
130
     *
131
     * Stored as an associative array keyed by the URL to avoid duplicates:
132
     *  - key: url
133
     *  - value: [url, scope, type]
134
     *
135
     * @var array<string, array{0:string,1:string,2:string}>
136
     */
137
    public array $documentsAddedInText = [];
138
139
    /**
140
     * Doctrine services.
141
     */
142
    private EntityManagerInterface $em;
143
    private CDocumentRepository $docRepo;
144
145
    /**
146
     * When exporting with a session context:
147
     * - true  => include both base course content + session content
148
     * - false => include only session-specific content
149
     *
150
     * This must be used consistently by all resource builders.
151
     */
152
    private bool $withBaseContent = false;
153
154
    /**
155
     * Constructor (keeps legacy init; wires Doctrine repositories).
156
     *
157
     * @param string     $type   'partial'|'complete'
158
     * @param array|null $course Optional course info array
159
     */
160
    public function __construct(string $type = '', ?array $course = null)
161
    {
162
        // Legacy behavior preserved
163
        $_course = api_get_course_info();
164
        if (!empty($course['official_code'])) {
165
            $_course = $course;
166
        }
167
168
        $this->course = new Course();
169
        $this->course->code = $_course['code'];
170
        $this->course->type = $type;
171
        $this->course->encoding = api_get_system_encoding();
172
        $this->course->info = $_course;
173
174
        /** @var EntityManagerInterface $em */
175
        $em = Database::getManager();
176
        $this->em = $em;
177
178
        /** @var CDocumentRepository $docRepo */
179
        $docRepo = Container::getDocumentRepository();
180
        $this->docRepo = $docRepo;
181
    }
182
183
    /**
184
     * Merge a parsed list of document refs into memory.
185
     *
186
     * @param array<int, array{0:string,1:string,2:string}> $list
187
     */
188
    public function addDocumentList(array $list): void
189
    {
190
        foreach ($list as $item) {
191
            $url = (string) ($item[0] ?? '');
192
            if ('' === $url) {
193
                continue;
194
            }
195
196
            // Keep the first occurrence for a given URL (dedupe by URL key).
197
            if (!isset($this->documentsAddedInText[$url])) {
198
                $this->documentsAddedInText[$url] = $item;
199
            }
200
        }
201
    }
202
203
    /**
204
     * Parse HTML and collect referenced course documents.
205
     */
206
    public function findAndSetDocumentsInText(string $html = ''): void
207
    {
208
        if ('' === $html) {
209
            return;
210
        }
211
        $documentList = DocumentManager::get_resources_from_source_html($html);
212
        $this->addDocumentList($documentList);
213
    }
214
215
    /**
216
     * Resolve collected HTML links to CDocument iids and build them.
217
     */
218
    public function restoreDocumentsFromList(?CourseEntity $course = null, ?SessionEntity $session = null): void
219
    {
220
        if (empty($this->documentsAddedInText)) {
221
            return;
222
        }
223
224
        // Resolve current course entity if not provided.
225
        if (!$course instanceof CourseEntity) {
226
            $courseInfo = api_get_course_info();
227
            $courseCode = (string) ($courseInfo['code'] ?? '');
228
            if ('' === $courseCode) {
229
                return;
230
            }
231
232
            /** @var CourseEntity|null $resolved */
233
            $resolved = $this->em->getRepository(CourseEntity::class)->findOneBy(['code' => $courseCode]);
234
            if (!$resolved instanceof CourseEntity) {
235
                return;
236
            }
237
238
            $course = $resolved;
239
        }
240
241
        $need = [];
242
243
        foreach ($this->documentsAddedInText as $item) {
244
            [$url, $scope, $type] = $item; // url, scope(local/remote), type(rel/abs/url)
245
246
            // Only process local document-style URLs.
247
            if ('local' !== $scope || !\in_array($type, ['rel', 'abs'], true)) {
248
                continue;
249
            }
250
251
            $rel = $this->extractRelativeDocumentPathFromUrl((string) $url);
252
            $rel = $this->normalizeDocumentRelPath($rel);
253
254
            if ('' === $rel) {
255
                continue;
256
            }
257
258
            // Include the file path itself.
259
            $need[$rel] = true;
260
261
            // Also include parent folders to preserve hierarchy on restore.
262
            $parts = array_values(array_filter(explode('/', $rel), static fn ($s) => '' !== $s));
263
            if (\count($parts) > 1) {
264
                $prefix = '';
265
                for ($i = 0; $i < \count($parts) - 1; $i++) {
266
                    $prefix = '' === $prefix ? $parts[$i] : $prefix.'/'.$parts[$i];
267
                    $need[$prefix] = true;
268
                }
269
            }
270
        }
271
272
        if (empty($need)) {
273
            return;
274
        }
275
276
        $paths = array_keys($need);
277
278
        $iids = $this->resolveDocumentIidsByRelativePaths($course, $session, $paths);
279
        if (empty($iids)) {
280
            error_log('COURSE_BUILD: no referenced documents matched in repository (paths_count='.\count($paths).')');
281
            return;
282
        }
283
284
        $sid = (int) ($session?->getId() ?? api_get_session_id());
285
        $cid = (int) $course->getId();
286
287
        $this->build_documents($sid, $cid, $this->withBaseContent, $iids);
288
    }
289
290
    /**
291
     * Extract path part after ".../document/" from a URL.
292
     * Returns an empty string when not a document-like URL.
293
     */
294
    private function extractRelativeDocumentPathFromUrl(string $url): string
295
    {
296
        if ('' === $url) {
297
            return '';
298
        }
299
300
        // Remove fragment/query early, keep only path.
301
        $decoded = urldecode($url);
302
        $path = (string) (parse_url($decoded, PHP_URL_PATH) ?? '');
303
        if ('' === $path) {
304
            $path = $decoded;
305
        }
306
307
        // Most common patterns:
308
        // - /courses/COURSECODE/document/Folder/file.png
309
        // - /document/Folder/file.png
310
        // - document/Folder/file.png
311
        $pos = stripos($path, '/document/');
312
        if (false !== $pos) {
313
            return substr($path, $pos + \strlen('/document/')) ?: '';
314
        }
315
316
        if (str_starts_with($path, 'document/')) {
317
            return substr($path, \strlen('document/')) ?: '';
318
        }
319
        if (str_starts_with($path, '/document/')) {
320
            return substr($path, \strlen('/document/')) ?: '';
321
        }
322
323
        // Fallback: "/document" without trailing slash.
324
        $pos2 = stripos($path, '/document');
325
        if (false !== $pos2) {
326
            $tail = substr($path, $pos2 + \strlen('/document')) ?: '';
327
            return ltrim($tail, '/');
328
        }
329
330
        return '';
331
    }
332
333
    /**
334
     * Normalize a relative document path for matching.
335
     */
336
    private function normalizeDocumentRelPath(string $path): string
337
    {
338
        if ('' === $path) {
339
            return '';
340
        }
341
342
        $path = urldecode($path);
343
        $path = str_replace('\\', '/', $path);
344
345
        // Remove "Documents" prefix if present (defensive).
346
        $path = preg_replace('~^/?Documents/?~i', '', $path) ?? $path;
347
348
        // Trim slashes and collapse duplicated slashes.
349
        $path = trim($path, '/');
350
        $path = preg_replace('~/{2,}~', '/', $path) ?? $path;
351
352
        return (string) $path;
353
    }
354
355
    /**
356
     * Resolve document IIDs by comparing computed "relative display paths" with the given list.
357
     *
358
     * @param array<int,string> $relativePaths
359
     *
360
     * @return array<int>
361
     */
362
    private function resolveDocumentIidsByRelativePaths(
363
        CourseEntity $course,
364
        ?SessionEntity $session,
365
        array $relativePaths
366
    ): array {
367
        if (empty($relativePaths)) {
368
            return [];
369
        }
370
371
        $need = [];
372
        foreach ($relativePaths as $p) {
373
            $p = $this->normalizeDocumentRelPath((string) $p);
374
            if ('' !== $p) {
375
                $need[$p] = true;
376
            }
377
        }
378
        if (empty($need)) {
379
            return [];
380
        }
381
382
        // IMPORTANT: always pass withBaseContent to the repository when supported.
383
        $qb = $this->getResourcesByCourseQbFromRepo($this->docRepo, $course, $session, $this->withBaseContent);
384
385
        /** @var CDocument[] $docs */
386
        $docs = $qb->getQuery()->getResult();
387
388
        $documentsRoot = $this->docRepo->getCourseDocumentsRootNode($course);
389
390
        $iids = [];
391
392
        foreach ($docs as $doc) {
393
            $node = $doc->getResourceNode();
394
            if (!$node instanceof ResourceNode) {
395
                continue;
396
            }
397
398
            $rel = '';
399
            if ($documentsRoot instanceof ResourceNode) {
400
                $rel = (string) $node->getPathForDisplayRemoveBase((string) $documentsRoot->getPath());
401
            } else {
402
                $rel = (string) $node->convertPathForDisplay((string) $node->getPath());
403
                $rel = preg_replace('~^/?Documents/?~i', '', (string) $rel) ?? $rel;
404
            }
405
406
            $rel = $this->normalizeDocumentRelPath($rel);
407
408
            if ('' === $rel) {
409
                continue;
410
            }
411
412
            if (isset($need[$rel])) {
413
                $iid = (int) $doc->getIid();
414
                if ($iid > 0) {
415
                    $iids[] = $iid;
416
                }
417
            }
418
        }
419
420
        return array_values(array_unique($iids));
421
    }
422
423
    /**
424
     * Set tools to build.
425
     *
426
     * @param array<string> $array
427
     */
428
    public function set_tools_to_build(array $array): void
429
    {
430
        $this->tools_to_build = $array;
431
    }
432
433
    /**
434
     * Set specific id list per tool.
435
     *
436
     * @param array<string, array<int>> $array
437
     */
438
    public function set_tools_specific_id_list(array $array): void
439
    {
440
        $this->specific_id_list = $array;
441
    }
442
443
    /**
444
     * Build the course (documents already repo-based; other tools preserved).
445
     *
446
     * @param array<int|string>   $parseOnlyToolList
447
     * @param array<string,mixed> $toolsFromPost
448
     */
449
    public function build(
450
        int $session_id = 0,
451
        string $courseCode = '',
452
        bool $withBaseContent = false,
453
        array $parseOnlyToolList = [],
454
        array $toolsFromPost = []
455
    ): Course {
456
        $this->withBaseContent = $withBaseContent;
457
458
        // Resolve effective course code:
459
        // - If caller did not pass a code, reuse the code already loaded in the constructor.
460
        $effectiveCourseCode = '' !== trim($courseCode)
461
            ? trim($courseCode)
462
            : (string) ($this->course->code ?? '');
463
464
        if ('' === $effectiveCourseCode) {
465
            throw new \RuntimeException('CourseBuilder cannot determine a course code (empty effective code).');
466
        }
467
468
        // Prefer constructor-provided course info to avoid api_get_course_info() side effects.
469
        if (empty($this->courseInfo) || !is_array($this->courseInfo)) {
470
            $this->courseInfo = is_array($this->course->info ?? null) ? $this->course->info : [];
471
        }
472
473
        // Only fetch via api_get_course_info() if we still don't have matching info.
474
        if (
475
            empty($this->courseInfo)
476
            || !is_array($this->courseInfo)
477
            || (string) ($this->courseInfo['code'] ?? '') !== $effectiveCourseCode
478
        ) {
479
            $this->courseInfo = api_get_course_info($effectiveCourseCode);
480
        }
481
482
        if (empty($this->courseInfo) || !is_array($this->courseInfo) || '' === (string) ($this->courseInfo['code'] ?? '')) {
483
            throw new \RuntimeException(sprintf(
484
                'CourseBuilder cannot load course info for course code "%s".',
485
                $effectiveCourseCode
486
            ));
487
        }
488
489
        /** @var CourseEntity|null $courseEntity */
490
        $courseEntity = $this->em->getRepository(CourseEntity::class)->findOneBy(['code' => $effectiveCourseCode]);
491
        if (!$courseEntity instanceof CourseEntity) {
492
            throw new \RuntimeException(sprintf(
493
                'CourseBuilder cannot resolve CourseEntity for course code "%s".',
494
                $effectiveCourseCode
495
            ));
496
        }
497
498
        /** @var SessionEntity|null $sessionEntity */
499
        $sessionEntity = $session_id
500
            ? $this->em->getRepository(SessionEntity::class)->find($session_id)
501
            : null;
502
503
        // Legacy DTO where resources[...] are built
504
        $legacyCourse = $this->course;
505
        foreach ($this->tools_to_build as $toolKey) {
506
            if (!empty($parseOnlyToolList)) {
507
                $const = $this->toolToName[$toolKey] ?? null;
508
                if (null !== $const && !\in_array($const, $parseOnlyToolList, true)) {
509
                    continue;
510
                }
511
            }
512
513
            if ('documents' === $toolKey) {
514
                $ids = $this->specific_id_list['documents'] ?? [];
515
                $this->build_documents_with_repo($courseEntity, $sessionEntity, $withBaseContent, $ids);
516
            }
517
518
            if ('forums' === $toolKey || 'forum' === $toolKey) {
519
                $ids = $this->specific_id_list['forums'] ?? $this->specific_id_list['forum'] ?? [];
520
                $this->build_forum_category($legacyCourse, $courseEntity, $sessionEntity, $ids);
521
                $this->build_forums($legacyCourse, $courseEntity, $sessionEntity, $ids);
522
                $this->build_forum_topics($legacyCourse, $courseEntity, $sessionEntity, $ids);
523
                $this->build_forum_posts($legacyCourse, $courseEntity, $sessionEntity, $ids);
524
            }
525
526
            if ('tool_intro' === $toolKey) {
527
                $this->build_tool_intro($legacyCourse, $courseEntity, $sessionEntity);
528
            }
529
530
            if ('links' === $toolKey) {
531
                $ids = $this->specific_id_list['links'] ?? [];
532
                $this->build_links($legacyCourse, $courseEntity, $sessionEntity, $ids);
533
            }
534
535
            if ('quizzes' === $toolKey || 'quiz' === $toolKey) {
536
                $ids = $this->specific_id_list['quizzes'] ?? $this->specific_id_list['quiz'] ?? [];
537
                $neededQuestionIds = $this->build_quizzes($legacyCourse, $courseEntity, $sessionEntity, $ids);
538
                // Always export question bucket required by the quizzes
539
                $this->build_quiz_questions($legacyCourse, $courseEntity, $sessionEntity, $neededQuestionIds);
540
            }
541
542
            if ('quiz_questions' === $toolKey) {
543
                $ids = $this->specific_id_list['quiz_questions'] ?? [];
544
                $this->build_quiz_questions($legacyCourse, $courseEntity, $sessionEntity, $ids);
545
            }
546
547
            if ('surveys' === $toolKey || 'survey' === $toolKey) {
548
                $ids = $this->specific_id_list['surveys'] ?? $this->specific_id_list['survey'] ?? [];
549
                $neededQ = $this->build_surveys($this->course, $courseEntity, $sessionEntity, $ids);
550
                $this->build_survey_questions($this->course, $courseEntity, $sessionEntity, $neededQ);
551
            }
552
553
            if ('survey_questions' === $toolKey) {
554
                $this->build_survey_questions($this->course, $courseEntity, $sessionEntity, []);
555
            }
556
557
            if ('announcements' === $toolKey) {
558
                $ids = $this->specific_id_list['announcements'] ?? [];
559
                $this->build_announcements($this->course, $courseEntity, $sessionEntity, $ids);
560
            }
561
562
            if ('events' === $toolKey) {
563
                $ids = $this->specific_id_list['events'] ?? [];
564
                $this->build_events($this->course, $courseEntity, $sessionEntity, $ids);
565
            }
566
567
            if ('course_descriptions' === $toolKey) {
568
                $ids = $this->specific_id_list['course_descriptions'] ?? [];
569
                $this->build_course_descriptions($this->course, $courseEntity, $sessionEntity, $ids);
570
            }
571
572
            if ('glossary' === $toolKey) {
573
                $ids = $this->specific_id_list['glossary'] ?? [];
574
                $this->build_glossary($this->course, $courseEntity, $sessionEntity, $ids);
575
            }
576
577
            if ('wiki' === $toolKey) {
578
                $ids = $this->specific_id_list['wiki'] ?? [];
579
                $this->build_wiki($this->course, $courseEntity, $sessionEntity, $ids);
580
            }
581
582
            if ('thematic' === $toolKey) {
583
                $ids = $this->specific_id_list['thematic'] ?? [];
584
                $this->build_thematic($this->course, $courseEntity, $sessionEntity, $ids);
585
            }
586
587
            if ('attendance' === $toolKey) {
588
                $ids = $this->specific_id_list['attendance'] ?? [];
589
                $this->build_attendance($this->course, $courseEntity, $sessionEntity, $ids);
590
            }
591
592
            if ('works' === $toolKey) {
593
                $ids = $this->specific_id_list['works'] ?? [];
594
                $this->build_works($this->course, $courseEntity, $sessionEntity, $ids);
595
            }
596
597
            if ('gradebook' === $toolKey) {
598
                $this->build_gradebook($this->course, $courseEntity, $sessionEntity);
599
            }
600
601
            if ('learnpath_category' === $toolKey) {
602
                $ids = $this->specific_id_list['learnpath_category'] ?? [];
603
                $this->build_learnpath_category($this->course, $courseEntity, $sessionEntity, $ids);
604
            }
605
606
            if ('learnpaths' === $toolKey) {
607
                $ids = $this->specific_id_list['learnpaths'] ?? [];
608
                $this->build_learnpaths($this->course, $courseEntity, $sessionEntity, $ids, true);
609
            }
610
        }
611
612
        // Always try to include documents referenced inside HTML (images, attachments, etc.).
613
        if ($courseEntity instanceof CourseEntity) {
0 ignored issues
show
introduced by
$courseEntity is always a sub-type of Chamilo\CoreBundle\Entity\Course.
Loading history...
614
            // Avoid pulling base documents into a session-only build (prevents duplicates on two-pass copy).
615
            if ($withBaseContent || null === $sessionEntity) {
616
                $this->restoreDocumentsFromList($courseEntity, $sessionEntity);
617
            }
618
        }
619
620
        return $this->course;
621
    }
622
623
    /**
624
     * Export Learnpath categories (CLpCategory).
625
     *
626
     * @param array<int> $ids
627
     */
628
    public function build_learnpath_category(
629
        object $legacyCourse,
630
        ?CourseEntity $courseEntity,
631
        ?SessionEntity $sessionEntity,
632
        array $ids
633
    ): void {
634
        if (!$courseEntity instanceof CourseEntity) {
635
            return;
636
        }
637
638
        $repo = Container::getLpCategoryRepository();
639
640
        // propagate withBaseContent to repo when supported.
641
        $qb = $this->getResourcesByCourseQbFromRepo($repo, $courseEntity, $sessionEntity, $this->withBaseContent);
642
643
        if (!empty($ids)) {
644
            $qb->andWhere('resource.iid IN (:ids)')
645
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
646
            ;
647
        }
648
649
        /** @var CLpCategory[] $rows */
650
        $rows = $qb->getQuery()->getResult();
651
652
        foreach ($rows as $cat) {
653
            $iid = (int) $cat->getIid();
654
            $title = (string) $cat->getTitle();
655
656
            $payload = [
657
                'id' => $iid,
658
                'title' => $title,
659
            ];
660
661
            $legacyCourse->resources[RESOURCE_LEARNPATH_CATEGORY][$iid] =
662
                $this->mkLegacyItem(RESOURCE_LEARNPATH_CATEGORY, $iid, $payload);
663
        }
664
    }
665
666
    /**
667
     * Export Learnpaths (CLp) + items, with optional SCORM folder packing.
668
     *
669
     * @param array<int> $idList
670
     */
671
    public function build_learnpaths(
672
        object $legacyCourse,
673
        ?CourseEntity $courseEntity,
674
        ?SessionEntity $sessionEntity,
675
        array $idList = [],
676
        bool $addScormFolder = true
677
    ): void {
678
        if (!$courseEntity instanceof CourseEntity) {
679
            return;
680
        }
681
682
        $lpRepo = Container::getLpRepository();
683
684
        // propagate withBaseContent to repo when supported.
685
        $qb = $this->getResourcesByCourseQbFromRepo($lpRepo, $courseEntity, $sessionEntity, $this->withBaseContent);
686
687
        if (!empty($idList)) {
688
            $qb->andWhere('resource.iid IN (:ids)')
689
                ->setParameter('ids', array_values(array_unique(array_map('intval', $idList))))
690
            ;
691
        }
692
693
        /** @var CLp[] $lps */
694
        $lps = $qb->getQuery()->getResult();
695
696
        foreach ($lps as $lp) {
697
            $iid = (int) $lp->getIid();
698
            $lpType = (int) $lp->getLpType(); // 1=LP, 2=SCORM, 3=AICC
699
700
            $items = [];
701
702
            /** @var CLpItem $it */
703
            foreach ($lp->getItems() as $it) {
704
                $items[] = [
705
                    'id' => (int) $it->getIid(),
706
                    'item_type' => (string) $it->getItemType(),
707
                    'ref' => (string) $it->getRef(),
708
                    'title' => (string) $it->getTitle(),
709
                    'name' => (string) $lp->getTitle(),
710
                    'description' => (string) ($it->getDescription() ?? ''),
711
                    'path' => (string) $it->getPath(),
712
                    'min_score' => (float) $it->getMinScore(),
713
                    'max_score' => null !== $it->getMaxScore() ? (float) $it->getMaxScore() : null,
714
                    'mastery_score' => null !== $it->getMasteryScore() ? (float) $it->getMasteryScore() : null,
715
                    'parent_item_id' => (int) $it->getParentItemId(),
716
                    'previous_item_id' => null !== $it->getPreviousItemId() ? (int) $it->getPreviousItemId() : null,
717
                    'next_item_id' => null !== $it->getNextItemId() ? (int) $it->getNextItemId() : null,
718
                    'display_order' => (int) $it->getDisplayOrder(),
719
                    'prerequisite' => (string) ($it->getPrerequisite() ?? ''),
720
                    'parameters' => (string) ($it->getParameters() ?? ''),
721
                    'launch_data' => (string) $it->getLaunchData(),
722
                    'audio' => (string) ($it->getAudio() ?? ''),
723
                ];
724
            }
725
726
            $payload = [
727
                'id' => $iid,
728
                'lp_type' => $lpType,
729
                'title' => (string) $lp->getTitle(),
730
                'path' => (string) $lp->getPath(),
731
                'ref' => (string) ($lp->getRef() ?? ''),
732
                'description' => (string) ($lp->getDescription() ?? ''),
733
                'content_local' => (string) $lp->getContentLocal(),
734
                'default_encoding' => (string) $lp->getDefaultEncoding(),
735
                'default_view_mod' => (string) $lp->getDefaultViewMod(),
736
                'prevent_reinit' => (bool) $lp->getPreventReinit(),
737
                'force_commit' => (bool) $lp->getForceCommit(),
738
                'content_maker' => (string) $lp->getContentMaker(),
739
                'display_order' => (int) $lp->getDisplayNotAllowedLp(),
740
                'js_lib' => (string) $lp->getJsLib(),
741
                'content_license' => (string) $lp->getContentLicense(),
742
                'debug' => (bool) $lp->getDebug(),
743
                'visibility' => '1',
744
                'author' => (string) $lp->getAuthor(),
745
                'use_max_score' => (int) $lp->getUseMaxScore(),
746
                'autolaunch' => (int) $lp->getAutolaunch(),
747
                'created_on' => $this->fmtDate($lp->getCreatedOn()),
748
                'modified_on' => $this->fmtDate($lp->getModifiedOn()),
749
                'published_on' => $this->fmtDate($lp->getPublishedOn()),
750
                'expired_on' => $this->fmtDate($lp->getExpiredOn()),
751
                'session_id' => (int) ($sessionEntity?->getId() ?? 0),
752
                'category_id' => (int) ($lp->getCategory()?->getIid() ?? 0),
753
                'items' => $items,
754
            ];
755
756
            $legacyCourse->resources[RESOURCE_LEARNPATH][$iid] =
757
                $this->mkLegacyItem(RESOURCE_LEARNPATH, $iid, $payload, ['items']);
758
        }
759
760
        // Optional: pack “scorm” folder (legacy parity)
761
        if ($addScormFolder && isset($this->course->backup_path)) {
762
            $scormDir = rtrim((string) $this->course->backup_path, '/').'/scorm';
763
            if (is_dir($scormDir) && ($dh = @opendir($scormDir))) {
764
                $i = 1;
765
                while (false !== ($file = readdir($dh))) {
766
                    if ('.' === $file || '..' === $file) {
767
                        continue;
768
                    }
769
                    if (is_dir($scormDir.'/'.$file)) {
770
                        $payload = ['path' => '/'.$file, 'name' => (string) $file];
771
                        $legacyCourse->resources['scorm'][$i] =
772
                            $this->mkLegacyItem('scorm', $i, $payload);
773
                        $i++;
774
                    }
775
                }
776
                closedir($dh);
777
            }
778
        }
779
    }
780
781
    /**
782
     * Export Gradebook (categories + evaluations + links).
783
     */
784
    public function build_gradebook(
785
        object $legacyCourse,
786
        ?CourseEntity $courseEntity,
787
        ?SessionEntity $sessionEntity
788
    ): void {
789
        if (!$courseEntity instanceof CourseEntity) {
790
            return;
791
        }
792
793
        /** @var EntityManagerInterface $em */
794
        $em = Database::getManager();
795
        $catRepo = $em->getRepository(GradebookCategory::class);
796
797
        $criteria = ['course' => $courseEntity];
798
        if ($sessionEntity) {
799
            $criteria['session'] = $sessionEntity;
800
        }
801
802
        /** @var GradebookCategory[] $cats */
803
        $cats = $catRepo->findBy($criteria);
804
        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...
805
            return;
806
        }
807
808
        $payloadCategories = [];
809
        foreach ($cats as $cat) {
810
            $payloadCategories[] = $this->serializeGradebookCategory($cat);
811
        }
812
813
        $backup = new GradeBookBackup($payloadCategories);
814
        $legacyCourse->add_resource($backup);
815
    }
816
817
    /**
818
     * Serialize GradebookCategory (and nested parts) to array for restore.
819
     *
820
     * @return array<string,mixed>
821
     */
822
    private function serializeGradebookCategory(GradebookCategory $c): array
823
    {
824
        $arr = [
825
            'id' => (int) $c->getId(),
826
            'title' => (string) $c->getTitle(),
827
            'description' => (string) ($c->getDescription() ?? ''),
828
            'weight' => (float) $c->getWeight(),
829
            'visible' => (bool) $c->getVisible(),
830
            'locked' => (int) $c->getLocked(),
831
            'parent_id' => $c->getParent() ? (int) $c->getParent()->getId() : 0,
832
            'generate_certificates' => (bool) $c->getGenerateCertificates(),
833
            'certificate_validity_period' => $c->getCertificateValidityPeriod(),
834
            'is_requirement' => (bool) $c->getIsRequirement(),
835
            'default_lowest_eval_exclude' => (bool) $c->getDefaultLowestEvalExclude(),
836
            'minimum_to_validate' => $c->getMinimumToValidate(),
837
            'gradebooks_to_validate_in_dependence' => $c->getGradeBooksToValidateInDependence(),
838
            'allow_skills_by_subcategory' => $c->getAllowSkillsBySubcategory(),
839
            // camelCase duplicates (future-proof)
840
            'generateCertificates' => (bool) $c->getGenerateCertificates(),
841
            'certificateValidityPeriod' => $c->getCertificateValidityPeriod(),
842
            'isRequirement' => (bool) $c->getIsRequirement(),
843
            'defaultLowestEvalExclude' => (bool) $c->getDefaultLowestEvalExclude(),
844
            'minimumToValidate' => $c->getMinimumToValidate(),
845
            'gradeBooksToValidateInDependence' => $c->getGradeBooksToValidateInDependence(),
846
            'allowSkillsBySubcategory' => $c->getAllowSkillsBySubcategory(),
847
        ];
848
849
        if ($c->getGradeModel()) {
850
            $arr['grade_model_id'] = (int) $c->getGradeModel()->getId();
851
        }
852
853
        // Evaluations
854
        $arr['evaluations'] = [];
855
        foreach ($c->getEvaluations() as $e) {
856
            /** @var GradebookEvaluation $e */
857
            $arr['evaluations'][] = [
858
                'title' => (string) $e->getTitle(),
859
                'description' => (string) ($e->getDescription() ?? ''),
860
                'weight' => (float) $e->getWeight(),
861
                'max' => (float) $e->getMax(),
862
                'type' => (string) $e->getType(),
863
                'visible' => (int) $e->getVisible(),
864
                'locked' => (int) $e->getLocked(),
865
                'best_score' => $e->getBestScore(),
866
                'average_score' => $e->getAverageScore(),
867
                'score_weight' => $e->getScoreWeight(),
868
                'min_score' => $e->getMinScore(),
869
            ];
870
        }
871
872
        // Links
873
        $arr['links'] = [];
874
        foreach ($c->getLinks() as $l) {
875
            /** @var GradebookLink $l */
876
            $arr['links'][] = [
877
                'type' => (int) $l->getType(),
878
                'ref_id' => (int) $l->getRefId(),
879
                'weight' => (float) $l->getWeight(),
880
                'visible' => (int) $l->getVisible(),
881
                'locked' => (int) $l->getLocked(),
882
                'best_score' => $l->getBestScore(),
883
                'average_score' => $l->getAverageScore(),
884
                'score_weight' => $l->getScoreWeight(),
885
                'min_score' => $l->getMinScore(),
886
            ];
887
        }
888
889
        return $arr;
890
    }
891
892
    /**
893
     * Export Works (root folders only; include assignment params).
894
     *
895
     * @param array<int> $ids
896
     */
897
    public function build_works(
898
        object $legacyCourse,
899
        ?CourseEntity $courseEntity,
900
        ?SessionEntity $sessionEntity,
901
        array $ids
902
    ): void {
903
        if (!$courseEntity instanceof CourseEntity) {
904
            return;
905
        }
906
907
        $repo = Container::getStudentPublicationRepository();
908
        $qb = $this->getResourcesByCourseQbFromRepo($repo, $courseEntity, $sessionEntity, $this->withBaseContent);
909
910
        $qb
911
            ->andWhere('resource.publicationParent IS NULL')
912
            ->andWhere('resource.filetype = :ft')->setParameter('ft', 'folder')
913
            ->andWhere('resource.active = 1')
914
        ;
915
916
        if (!empty($ids)) {
917
            $qb->andWhere('resource.iid IN (:ids)')
918
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
919
            ;
920
        }
921
922
        /** @var CStudentPublication[] $rows */
923
        $rows = $qb->getQuery()->getResult();
924
925
        foreach ($rows as $row) {
926
            $iid = (int) $row->getIid();
927
            $title = (string) $row->getTitle();
928
            $desc = (string) ($row->getDescription() ?? '');
929
930
            // Detect documents linked in description
931
            $this->findAndSetDocumentsInText($desc);
932
933
            $asgmt = $row->getAssignment();
934
            $expiresOn = $asgmt?->getExpiresOn()?->format('Y-m-d H:i:s');
935
            $endsOn = $asgmt?->getEndsOn()?->format('Y-m-d H:i:s');
936
            $addToCal = $asgmt && $asgmt->getEventCalendarId() > 0 ? 1 : 0;
937
            $enableQ = (bool) ($asgmt?->getEnableQualification() ?? false);
938
939
            $params = [
940
                'id' => $iid,
941
                'title' => $title,
942
                'description' => $desc,
943
                'weight' => (float) $row->getWeight(),
944
                'qualification' => (float) $row->getQualification(),
945
                'allow_text_assignment' => (int) $row->getAllowTextAssignment(),
946
                'default_visibility' => (bool) ($row->getDefaultVisibility() ?? false),
947
                'student_delete_own_publication' => (bool) ($row->getStudentDeleteOwnPublication() ?? false),
948
                'extensions' => $row->getExtensions(),
949
                'group_category_work_id' => (int) $row->getGroupCategoryWorkId(),
950
                'post_group_id' => (int) $row->getPostGroupId(),
951
                'enable_qualification' => $enableQ,
952
                'add_to_calendar' => $addToCal ? 1 : 0,
953
                'expires_on' => $expiresOn ?: null,
954
                'ends_on' => $endsOn ?: null,
955
                'name' => $title,
956
                'url' => null,
957
            ];
958
959
            $legacy = new Work($params);
960
            $legacyCourse->add_resource($legacy);
961
        }
962
    }
963
964
    /**
965
     * Export Attendance + calendars.
966
     *
967
     * @param array<int> $ids
968
     */
969
    public function build_attendance(
970
        object $legacyCourse,
971
        ?CourseEntity $courseEntity,
972
        ?SessionEntity $sessionEntity,
973
        array $ids
974
    ): void {
975
        if (!$courseEntity instanceof CourseEntity) {
976
            return;
977
        }
978
979
        $repo = Container::getAttendanceRepository();
980
        $qb = $this->getResourcesByCourseQbFromRepo($repo, $courseEntity, $sessionEntity, $this->withBaseContent);
981
982
        if (!empty($ids)) {
983
            $qb->andWhere('resource.iid IN (:ids)')
984
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
985
            ;
986
        }
987
988
        /** @var CAttendance[] $rows */
989
        $rows = $qb->getQuery()->getResult();
990
991
        foreach ($rows as $row) {
992
            $iid = (int) $row->getIid();
993
            $title = (string) $row->getTitle();
994
            $desc = (string) ($row->getDescription() ?? '');
995
            $active = (int) $row->getActive();
996
997
            $this->findAndSetDocumentsInText($desc);
998
999
            $params = [
1000
                'id' => $iid,
1001
                'title' => $title,
1002
                'description' => $desc,
1003
                'active' => $active,
1004
                'attendance_qualify_title' => (string) ($row->getAttendanceQualifyTitle() ?? ''),
1005
                'attendance_qualify_max' => (int) $row->getAttendanceQualifyMax(),
1006
                'attendance_weight' => (float) $row->getAttendanceWeight(),
1007
                'locked' => (int) $row->getLocked(),
1008
                'name' => $title,
1009
            ];
1010
1011
            $legacy = new Attendance($params);
1012
1013
            /** @var CAttendanceCalendar $cal */
1014
            foreach ($row->getCalendars() as $cal) {
1015
                $calArr = [
1016
                    'id' => (int) $cal->getIid(),
1017
                    'attendance_id' => $iid,
1018
                    'date_time' => $cal->getDateTime()?->format('Y-m-d H:i:s') ?? '',
1019
                    'done_attendance' => (bool) $cal->getDoneAttendance(),
1020
                    'blocked' => (bool) $cal->getBlocked(),
1021
                    'duration' => null !== $cal->getDuration() ? (int) $cal->getDuration() : null,
1022
                ];
1023
                $legacy->add_attendance_calendar($calArr);
1024
            }
1025
1026
            $legacyCourse->add_resource($legacy);
1027
        }
1028
    }
1029
1030
    /**
1031
     * Export Thematic + advances + plans (and collect linked docs).
1032
     *
1033
     * @param array<int> $ids
1034
     */
1035
    public function build_thematic(
1036
        object $legacyCourse,
1037
        ?CourseEntity $courseEntity,
1038
        ?SessionEntity $sessionEntity,
1039
        array $ids
1040
    ): void {
1041
        if (!$courseEntity instanceof CourseEntity) {
1042
            return;
1043
        }
1044
1045
        $repo = Container::getThematicRepository();
1046
        $qb = $this->getResourcesByCourseQbFromRepo($repo, $courseEntity, $sessionEntity, $this->withBaseContent);
1047
1048
        if (!empty($ids)) {
1049
            $qb->andWhere('resource.iid IN (:ids)')
1050
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1051
            ;
1052
        }
1053
1054
        /** @var CThematic[] $rows */
1055
        $rows = $qb->getQuery()->getResult();
1056
1057
        foreach ($rows as $row) {
1058
            $iid = (int) $row->getIid();
1059
            $title = (string) $row->getTitle();
1060
            $content = (string) ($row->getContent() ?? '');
1061
            $active = (bool) $row->getActive();
1062
1063
            $this->findAndSetDocumentsInText($content);
1064
1065
            $params = [
1066
                'id' => $iid,
1067
                'title' => $title,
1068
                'content' => $content,
1069
                'active' => $active,
1070
            ];
1071
1072
            $legacy = new Thematic($params);
1073
1074
            /** @var CThematicAdvance $adv */
1075
            foreach ($row->getAdvances() as $adv) {
1076
                $attendanceId = 0;
1077
1078
                try {
1079
                    $refAtt = new ReflectionProperty(CThematicAdvance::class, 'attendance');
1080
                    if ($refAtt->isInitialized($adv)) {
1081
                        $att = $adv->getAttendance();
1082
                        if ($att) {
1083
                            $attendanceId = (int) $att->getIid();
1084
                        }
1085
                    }
1086
                } catch (Throwable) {
1087
                    // keep $attendanceId = 0
1088
                }
1089
1090
                $advArr = [
1091
                    'id' => (int) $adv->getIid(),
1092
                    'thematic_id' => (int) $row->getIid(),
1093
                    'content' => (string) ($adv->getContent() ?? ''),
1094
                    'start_date' => $adv->getStartDate()?->format('Y-m-d H:i:s') ?? '',
1095
                    'duration' => (int) $adv->getDuration(),
1096
                    'done_advance' => (bool) $adv->getDoneAdvance(),
1097
                    'attendance_id' => $attendanceId,
1098
                    'room_id' => (int) ($adv->getRoom()?->getId() ?? 0),
1099
                ];
1100
1101
                $this->findAndSetDocumentsInText((string) $advArr['content']);
1102
                $legacy->addThematicAdvance($advArr);
1103
            }
1104
1105
            /** @var CThematicPlan $pl */
1106
            foreach ($row->getPlans() as $pl) {
1107
                $plArr = [
1108
                    'id' => (int) $pl->getIid(),
1109
                    'thematic_id' => $iid,
1110
                    'title' => (string) $pl->getTitle(),
1111
                    'description' => (string) ($pl->getDescription() ?? ''),
1112
                    'description_type' => (int) $pl->getDescriptionType(),
1113
                ];
1114
                $this->findAndSetDocumentsInText((string) $plArr['description']);
1115
                $legacy->addThematicPlan($plArr);
1116
            }
1117
1118
            $legacyCourse->add_resource($legacy);
1119
        }
1120
    }
1121
1122
    /**
1123
     * Export Wiki pages (content + metadata; collect docs in content).
1124
     *
1125
     * @param array<int> $ids
1126
     */
1127
    public function build_wiki(
1128
        object $legacyCourse,
1129
        ?CourseEntity $courseEntity,
1130
        ?SessionEntity $sessionEntity,
1131
        array $ids
1132
    ): void {
1133
        if (!$courseEntity instanceof CourseEntity) {
1134
            return;
1135
        }
1136
1137
        $repo = Container::getWikiRepository();
1138
        $qb = $this->getResourcesByCourseQbFromRepo($repo, $courseEntity, $sessionEntity, $this->withBaseContent);
1139
1140
        if (!empty($ids)) {
1141
            $qb->andWhere('resource.iid IN (:ids)')
1142
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1143
            ;
1144
        }
1145
1146
        /** @var CWiki[] $pages */
1147
        $pages = $qb->getQuery()->getResult();
1148
1149
        foreach ($pages as $page) {
1150
            $iid = (int) $page->getIid();
1151
            $pageId = (int) ($page->getPageId() ?? $iid);
1152
            $reflink = (string) $page->getReflink();
1153
            $title = (string) $page->getTitle();
1154
            $content = (string) $page->getContent();
1155
            $userId = (int) $page->getUserId();
1156
            $groupId = (int) ($page->getGroupId() ?? 0);
1157
            $progress = (string) ($page->getProgress() ?? '');
1158
            $version = (int) ($page->getVersion() ?? 1);
1159
            $dtime = $page->getDtime()?->format('Y-m-d H:i:s') ?? '';
1160
1161
            $this->findAndSetDocumentsInText($content);
1162
1163
            $legacy = new Wiki(
1164
                $iid,
1165
                $pageId,
1166
                $reflink,
1167
                $title,
1168
                $content,
1169
                $userId,
1170
                $groupId,
1171
                $dtime,
1172
                $progress,
1173
                $version
1174
            );
1175
1176
            $this->course->add_resource($legacy);
1177
        }
1178
    }
1179
1180
    /**
1181
     * Export Glossary terms (collect docs in descriptions).
1182
     *
1183
     * @param array<int> $ids
1184
     */
1185
    public function build_glossary(
1186
        object $legacyCourse,
1187
        ?CourseEntity $courseEntity,
1188
        ?SessionEntity $sessionEntity,
1189
        array $ids
1190
    ): void {
1191
        if (!$courseEntity instanceof CourseEntity) {
1192
            return;
1193
        }
1194
1195
        $repo = Container::getGlossaryRepository();
1196
        $qb = $this->getResourcesByCourseQbFromRepo($repo, $courseEntity, $sessionEntity, $this->withBaseContent);
1197
1198
        if (!empty($ids)) {
1199
            $qb->andWhere('resource.iid IN (:ids)')
1200
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1201
            ;
1202
        }
1203
1204
        /** @var CGlossary[] $terms */
1205
        $terms = $qb->getQuery()->getResult();
1206
1207
        foreach ($terms as $term) {
1208
            $iid = (int) $term->getIid();
1209
            $title = (string) $term->getTitle();
1210
            $desc = (string) ($term->getDescription() ?? '');
1211
1212
            $this->findAndSetDocumentsInText($desc);
1213
1214
            $legacy = new Glossary($iid, $title, $desc, 0);
1215
            $this->course->add_resource($legacy);
1216
        }
1217
    }
1218
1219
    /**
1220
     * Export Course descriptions (collect docs in HTML).
1221
     *
1222
     * @param array<int> $ids
1223
     */
1224
    public function build_course_descriptions(
1225
        object $legacyCourse,
1226
        ?CourseEntity $courseEntity,
1227
        ?SessionEntity $sessionEntity,
1228
        array $ids
1229
    ): void {
1230
        if (!$courseEntity instanceof CourseEntity) {
1231
            return;
1232
        }
1233
1234
        $repo = Container::getCourseDescriptionRepository();
1235
        $qb = $this->getResourcesByCourseQbFromRepo($repo, $courseEntity, $sessionEntity, $this->withBaseContent);
1236
1237
        if (!empty($ids)) {
1238
            $qb->andWhere('resource.iid IN (:ids)')
1239
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1240
            ;
1241
        }
1242
1243
        /** @var CCourseDescription[] $rows */
1244
        $rows = $qb->getQuery()->getResult();
1245
1246
        foreach ($rows as $row) {
1247
            $iid = (int) $row->getIid();
1248
            $title = (string) ($row->getTitle() ?? '');
1249
            $html = (string) ($row->getContent() ?? '');
1250
            $type = (int) $row->getDescriptionType();
1251
1252
            $this->findAndSetDocumentsInText($html);
1253
1254
            $export = new CourseDescription($iid, $title, $html, $type);
1255
            $this->course->add_resource($export);
1256
        }
1257
    }
1258
1259
    /**
1260
     * Export Calendar events (first attachment as legacy, all as assets).
1261
     *
1262
     * @param array<int> $ids
1263
     */
1264
    public function build_events(
1265
        object $legacyCourse,
1266
        ?CourseEntity $courseEntity,
1267
        ?SessionEntity $sessionEntity,
1268
        array $ids
1269
    ): void {
1270
        if (!$courseEntity instanceof CourseEntity) {
1271
            return;
1272
        }
1273
1274
        $eventRepo = Container::getCalendarEventRepository();
1275
        $qb = $this->getResourcesByCourseQbFromRepo($eventRepo, $courseEntity, $sessionEntity, $this->withBaseContent);
1276
1277
        if (!empty($ids)) {
1278
            $qb->andWhere('resource.iid IN (:ids)')
1279
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1280
            ;
1281
        }
1282
1283
        /** @var CCalendarEvent[] $events */
1284
        $events = $qb->getQuery()->getResult();
1285
1286
        /** @var KernelInterface $kernel */
1287
        $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

1287
        /** @scrutinizer ignore-call */ 
1288
        $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...
1288
        $projectDir = rtrim($kernel->getProjectDir(), '/');
1289
        $resourceBase = $projectDir.'/var/upload/resource';
1290
1291
        /** @var ResourceNodeRepository $rnRepo */
1292
        $rnRepo = Container::$container->get(ResourceNodeRepository::class);
1293
1294
        foreach ($events as $ev) {
1295
            $iid = (int) $ev->getIid();
1296
            $title = (string) $ev->getTitle();
1297
            $content = (string) ($ev->getContent() ?? '');
1298
            $startDate = $ev->getStartDate()?->format('Y-m-d H:i:s') ?? '';
1299
            $endDate = $ev->getEndDate()?->format('Y-m-d H:i:s') ?? '';
1300
            $allDay = (int) $ev->isAllDay();
1301
1302
            $firstPath = $firstName = $firstComment = '';
1303
            $firstSize = 0;
1304
1305
            /** @var CCalendarEventAttachment $att */
1306
            foreach ($ev->getAttachments() as $att) {
1307
                $node = $att->getResourceNode();
1308
                $abs = null;
1309
                $size = 0;
1310
                $relForZip = null;
1311
1312
                if ($node) {
1313
                    $file = $node->getFirstResourceFile();
1314
                    if ($file) {
1315
                        $storedRel = (string) $rnRepo->getFilename($file);
1316
                        if ('' !== $storedRel) {
1317
                            $candidate = $resourceBase.$storedRel;
1318
                            if (is_readable($candidate)) {
1319
                                $abs = $candidate;
1320
                                $size = (int) $file->getSize();
1321
                                if ($size <= 0 && is_file($candidate)) {
1322
                                    $st = @stat($candidate);
1323
                                    $size = $st ? (int) $st['size'] : 0;
1324
                                }
1325
                                $base = basename($storedRel) ?: (string) $att->getIid();
1326
                                $relForZip = 'upload/calendar/'.$base;
1327
                            }
1328
                        }
1329
                    }
1330
                }
1331
1332
                if ($abs && $relForZip) {
1333
                    $this->tryAddAsset($relForZip, $abs, $size);
1334
                } else {
1335
                    error_log('COURSE_BUILD: event attachment file not found (event_iid='
1336
                        .$iid.'; att_iid='.(int) $att->getIid().')');
1337
                }
1338
1339
                if ('' === $firstName && $relForZip) {
1340
                    $firstPath = substr($relForZip, \strlen('upload/calendar/'));
1341
                    $firstName = (string) $att->getFilename();
1342
                    $firstComment = (string) ($att->getComment() ?? '');
1343
                    $firstSize = (int) $size;
1344
                }
1345
            }
1346
1347
            $export = new CalendarEvent(
1348
                $iid,
1349
                $title,
1350
                $content,
1351
                $startDate,
1352
                $endDate,
1353
                $firstPath,
1354
                $firstName,
1355
                $firstSize,
1356
                $firstComment,
1357
                $allDay
1358
            );
1359
1360
            $this->course->add_resource($export);
1361
        }
1362
    }
1363
1364
    /**
1365
     * Export Announcements (first attachment legacy, all as assets).
1366
     *
1367
     * @param array<int> $ids
1368
     */
1369
    public function build_announcements(
1370
        object $legacyCourse,
1371
        ?CourseEntity $courseEntity,
1372
        ?SessionEntity $sessionEntity,
1373
        array $ids
1374
    ): void {
1375
        if (!$courseEntity instanceof CourseEntity) {
1376
            return;
1377
        }
1378
1379
        $annRepo = Container::getAnnouncementRepository();
1380
        $qb = $this->getResourcesByCourseQbFromRepo($annRepo, $courseEntity, $sessionEntity, $this->withBaseContent);
1381
1382
        if (!empty($ids)) {
1383
            $qb->andWhere('resource.iid IN (:ids)')
1384
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))))
1385
            ;
1386
        }
1387
1388
        /** @var CAnnouncement[] $anns */
1389
        $anns = $qb->getQuery()->getResult();
1390
1391
        /** @var KernelInterface $kernel */
1392
        $kernel = Container::$container->get('kernel');
1393
        $projectDir = rtrim($kernel->getProjectDir(), '/');
1394
        $resourceBase = $projectDir.'/var/upload/resource';
1395
1396
        /** @var ResourceNodeRepository $rnRepo */
1397
        $rnRepo = Container::$container->get(ResourceNodeRepository::class);
1398
1399
        foreach ($anns as $a) {
1400
            $iid = (int) $a->getIid();
1401
            $title = (string) $a->getTitle();
1402
            $html = (string) ($a->getContent() ?? '');
1403
            $date = $a->getEndDate()?->format('Y-m-d H:i:s') ?? '';
1404
            $email = (bool) $a->getEmailSent();
1405
1406
            $firstPath = $firstName = $firstComment = '';
1407
            $firstSize = 0;
1408
1409
            $attachmentsArr = [];
1410
1411
            /** @var CAnnouncementAttachment $att */
1412
            foreach ($a->getAttachments() as $att) {
1413
                $relPath = ltrim((string) $att->getPath(), '/');
1414
                $assetRel = 'upload/announcements/'.$relPath;
1415
1416
                $abs = null;
1417
                $node = $att->getResourceNode();
1418
                if ($node) {
1419
                    $file = $node->getFirstResourceFile();
1420
                    if ($file) {
1421
                        $storedRel = (string) $rnRepo->getFilename($file);
1422
                        if ('' !== $storedRel) {
1423
                            $candidate = $resourceBase.$storedRel;
1424
                            if (is_readable($candidate)) {
1425
                                $abs = $candidate;
1426
                            }
1427
                        }
1428
                    }
1429
                }
1430
1431
                if ($abs) {
1432
                    $this->tryAddAsset($assetRel, $abs, (int) $att->getSize());
1433
                } else {
1434
                    error_log('COURSE_BUILD: announcement attachment not found (iid='.(int) $att->getIid().')');
1435
                }
1436
1437
                $attachmentsArr[] = [
1438
                    'path' => $relPath,
1439
                    'filename' => (string) $att->getFilename(),
1440
                    'size' => (int) $att->getSize(),
1441
                    'comment' => (string) ($att->getComment() ?? ''),
1442
                    'asset_relpath' => $assetRel,
1443
                ];
1444
1445
                if ('' === $firstName) {
1446
                    $firstPath = $relPath;
1447
                    $firstName = (string) $att->getFilename();
1448
                    $firstSize = (int) $att->getSize();
1449
                    $firstComment = (string) ($att->getComment() ?? '');
1450
                }
1451
            }
1452
1453
            $payload = [
1454
                'title' => $title,
1455
                'content' => $html,
1456
                'date' => $date,
1457
                'display_order' => 0,
1458
                'email_sent' => $email ? 1 : 0,
1459
                'attachment_path' => $firstPath,
1460
                'attachment_filename' => $firstName,
1461
                'attachment_size' => $firstSize,
1462
                'attachment_comment' => $firstComment,
1463
                'attachments' => $attachmentsArr,
1464
            ];
1465
1466
            $legacyCourse->resources[RESOURCE_ANNOUNCEMENT][$iid] =
1467
                $this->mkLegacyItem(RESOURCE_ANNOUNCEMENT, $iid, $payload, ['attachments']);
1468
        }
1469
    }
1470
1471
    /**
1472
     * Register an asset to be packed into the export ZIP.
1473
     */
1474
    private function addAsset(string $relPath, string $absPath, int $size = 0): void
1475
    {
1476
        if (!isset($this->course->resources['asset']) || !\is_array($this->course->resources['asset'])) {
1477
            $this->course->resources['asset'] = [];
1478
        }
1479
        $this->course->resources['asset'][$relPath] = [
1480
            'abs' => $absPath,
1481
            'size' => $size,
1482
        ];
1483
    }
1484
1485
    /**
1486
     * Try to add an asset only if file exists.
1487
     */
1488
    private function tryAddAsset(string $relPath, string $absPath, int $size = 0): void
1489
    {
1490
        if (is_file($absPath) && is_readable($absPath)) {
1491
            $this->addAsset($relPath, $absPath, $size);
1492
        } else {
1493
            error_log('COURSE_BUILD: asset missing: '.$absPath);
1494
        }
1495
    }
1496
1497
    /**
1498
     * Export Surveys; returns needed Question IDs for follow-up export.
1499
     *
1500
     * @param array<int> $surveyIds
1501
     *
1502
     * @return array<int>
1503
     */
1504
    public function build_surveys(
1505
        object $legacyCourse,
1506
        ?CourseEntity $courseEntity,
1507
        ?SessionEntity $sessionEntity,
1508
        array $surveyIds
1509
    ): array {
1510
        if (!$courseEntity) {
1511
            return [];
1512
        }
1513
1514
        // Context-aware query once (course/session + visibility rules).
1515
        $qb = $this->createContextQb(
1516
            CSurvey::class,
1517
            $courseEntity,
1518
            $sessionEntity,
1519
            's'
1520
        );
1521
1522
        if (!empty($surveyIds)) {
1523
            $qb->andWhere('s.iid IN (:ids)')
1524
                ->setParameter('ids', array_map('intval', $surveyIds));
1525
        }
1526
1527
        /** @var CSurvey[] $surveys */
1528
        $surveys = $qb->getQuery()->getResult();
1529
1530
        $neededQuestionIds = [];
1531
1532
        foreach ($surveys as $s) {
1533
            $iid = (int) $s->getIid();
1534
            $qIds = [];
1535
1536
            foreach ($s->getQuestions() as $q) {
1537
                /** @var CSurveyQuestion $q */
1538
                $qid = (int) $q->getIid();
1539
                $qIds[] = $qid;
1540
                $neededQuestionIds[$qid] = true;
1541
            }
1542
1543
            $payload = [
1544
                'code' => (string) ($s->getCode() ?? ''),
1545
                'title' => (string) $s->getTitle(),
1546
                'subtitle' => (string) ($s->getSubtitle() ?? ''),
1547
                'author' => '',
1548
                'lang' => (string) ($s->getLang() ?? ''),
1549
                'avail_from' => $s->getAvailFrom()?->format('Y-m-d H:i:s'),
1550
                'avail_till' => $s->getAvailTill()?->format('Y-m-d H:i:s'),
1551
                'is_shared' => (string) ($s->getIsShared() ?? '0'),
1552
                'template' => (string) ($s->getTemplate() ?? 'template'),
1553
                'intro' => (string) ($s->getIntro() ?? ''),
1554
                'surveythanks' => (string) ($s->getSurveythanks() ?? ''),
1555
                'creation_date' => $s->getCreationDate()?->format('Y-m-d H:i:s') ?: date('Y-m-d H:i:s'),
1556
                'invited' => (int) $s->getInvited(),
1557
                'answered' => (int) $s->getAnswered(),
1558
                'invite_mail' => (string) $s->getInviteMail(),
1559
                'reminder_mail' => (string) $s->getReminderMail(),
1560
                'mail_subject' => (string) $s->getMailSubject(),
1561
                'anonymous' => (string) $s->getAnonymous(),
1562
                'shuffle' => (bool) $s->getShuffle(),
1563
                'one_question_per_page' => (bool) $s->getOneQuestionPerPage(),
1564
                'visible_results' => $s->getVisibleResults(),
1565
                'display_question_number' => (bool) $s->isDisplayQuestionNumber(),
1566
                'survey_type' => (int) $s->getSurveyType(),
1567
                'show_form_profile' => (int) $s->getShowFormProfile(),
1568
                'form_fields' => (string) $s->getFormFields(),
1569
                'duration' => $s->getDuration(),
1570
                'question_ids' => $qIds,
1571
                'survey_id' => $iid,
1572
            ];
1573
1574
            $legacyCourse->resources[RESOURCE_SURVEY][$iid] =
1575
                $this->mkLegacyItem(RESOURCE_SURVEY, $iid, $payload);
1576
1577
            error_log('COURSE_BUILD: SURVEY iid='.$iid.' qids=['.implode(',', $qIds).']');
1578
        }
1579
1580
        return array_keys($neededQuestionIds);
1581
    }
1582
1583
    /**
1584
     * Export Survey Questions (answers promoted at top level).
1585
     *
1586
     * @param array<int> $questionIds
1587
     */
1588
    public function build_survey_questions(
1589
        object $legacyCourse,
1590
        ?CourseEntity $courseEntity,
1591
        ?SessionEntity $sessionEntity,
1592
        array $questionIds
1593
    ): void {
1594
        if (!$courseEntity) {
1595
            return;
1596
        }
1597
1598
        $qb = $this->em->createQueryBuilder()
1599
            ->select('q', 's')
1600
            ->from(CSurveyQuestion::class, 'q')
1601
            ->innerJoin('q.survey', 's')
1602
        ;
1603
1604
        // Apply the same context rules on the survey alias "s".
1605
        $this->applyContextToQb(
1606
            $qb,
1607
            's',
1608
            $courseEntity,
1609
            $sessionEntity
1610
        );
1611
1612
        $qb->distinct()
1613
            ->orderBy('s.iid', 'ASC')
1614
            ->addOrderBy('q.sort', 'ASC')
1615
        ;
1616
1617
        if (!empty($questionIds)) {
1618
            $qb->andWhere('q.iid IN (:ids)')
1619
                ->setParameter('ids', array_map('intval', $questionIds));
1620
        }
1621
1622
        /** @var CSurveyQuestion[] $questions */
1623
        $questions = $qb->getQuery()->getResult();
1624
1625
        $exported = 0;
1626
1627
        foreach ($questions as $q) {
1628
            $qid = (int) $q->getIid();
1629
            $sid = (int) $q->getSurvey()->getIid();
1630
1631
            $answers = [];
1632
            foreach ($q->getOptions() as $opt) {
1633
                /** @var CSurveyQuestionOption $opt */
1634
                $answers[] = [
1635
                    'option_text' => (string) $opt->getOptionText(),
1636
                    'sort' => (int) $opt->getSort(),
1637
                    'value' => (int) $opt->getValue(),
1638
                ];
1639
            }
1640
1641
            $payload = [
1642
                'survey_id' => $sid,
1643
                'survey_question' => (string) $q->getSurveyQuestion(),
1644
                'survey_question_comment' => (string) ($q->getSurveyQuestionComment() ?? ''),
1645
                'type' => (string) $q->getType(),
1646
                'display' => (string) $q->getDisplay(),
1647
                'sort' => (int) $q->getSort(),
1648
                'shared_question_id' => $q->getSharedQuestionId(),
1649
                'max_value' => $q->getMaxValue(),
1650
                'is_required' => (bool) $q->isMandatory(),
1651
                'answers' => $answers,
1652
            ];
1653
1654
            $legacyCourse->resources[RESOURCE_SURVEYQUESTION][$qid] =
1655
                $this->mkLegacyItem(RESOURCE_SURVEYQUESTION, $qid, $payload, ['answers']);
1656
1657
            $exported++;
1658
            error_log('COURSE_BUILD: SURVEY_Q qid='.$qid.' survey='.$sid.' answers='.\count($answers));
1659
        }
1660
1661
        error_log('COURSE_BUILD: survey questions exported='.$exported);
1662
    }
1663
1664
    /**
1665
     * Export Quizzes and return required Question IDs.
1666
     *
1667
     * @param array<int> $quizIds
1668
     *
1669
     * @return array<int>
1670
     */
1671
    public function build_quizzes(
1672
        object $legacyCourse,
1673
        ?CourseEntity $courseEntity,
1674
        ?SessionEntity $sessionEntity,
1675
        array $quizIds
1676
    ): array {
1677
        if (!$courseEntity) {
1678
            return [];
1679
        }
1680
1681
        $qb = $this->createContextQb(
1682
            CQuiz::class,
1683
            $courseEntity,
1684
            $sessionEntity,
1685
            'q'
1686
        );
1687
1688
        if (!empty($quizIds)) {
1689
            $qb->andWhere('q.iid IN (:ids)')
1690
                ->setParameter('ids', array_map('intval', $quizIds));
1691
        }
1692
1693
        /** @var CQuiz[] $quizzes */
1694
        $quizzes = $qb->getQuery()->getResult();
1695
        $neededQuestionIds = [];
1696
1697
        foreach ($quizzes as $quiz) {
1698
            $iid = (int) $quiz->getIid();
1699
1700
            $payload = [
1701
                'title' => (string) $quiz->getTitle(),
1702
                'description' => (string) ($quiz->getDescription() ?? ''),
1703
                'type' => (int) $quiz->getType(),
1704
                'random' => (int) $quiz->getRandom(),
1705
                'random_answers' => (bool) $quiz->getRandomAnswers(),
1706
                'results_disabled' => (int) $quiz->getResultsDisabled(),
1707
                'max_attempt' => (int) $quiz->getMaxAttempt(),
1708
                'feedback_type' => (int) $quiz->getFeedbackType(),
1709
                'expired_time' => (int) $quiz->getExpiredTime(),
1710
                'review_answers' => (int) $quiz->getReviewAnswers(),
1711
                'random_by_category' => (int) $quiz->getRandomByCategory(),
1712
                'text_when_finished' => (string) ($quiz->getTextWhenFinished() ?? ''),
1713
                'text_when_finished_failure' => (string) ($quiz->getTextWhenFinishedFailure() ?? ''),
1714
                'display_category_name' => (int) $quiz->getDisplayCategoryName(),
1715
                'save_correct_answers' => (int) ($quiz->getSaveCorrectAnswers() ?? 0),
1716
                'propagate_neg' => (int) $quiz->getPropagateNeg(),
1717
                'hide_question_title' => (bool) $quiz->isHideQuestionTitle(),
1718
                'hide_question_number' => (int) $quiz->getHideQuestionNumber(),
1719
                'question_selection_type' => (int) ($quiz->getQuestionSelectionType() ?? 0),
1720
                'access_condition' => (string) ($quiz->getAccessCondition() ?? ''),
1721
                'pass_percentage' => $quiz->getPassPercentage(),
1722
                'start_time' => $quiz->getStartTime()?->format('Y-m-d H:i:s'),
1723
                'end_time' => $quiz->getEndTime()?->format('Y-m-d H:i:s'),
1724
                'question_ids' => [],
1725
                'question_orders' => [],
1726
            ];
1727
1728
            $rels = $this->em->createQueryBuilder()
1729
                ->select('rel', 'qq')
1730
                ->from(CQuizRelQuestion::class, 'rel')
1731
                ->innerJoin('rel.question', 'qq')
1732
                ->andWhere('rel.quiz = :quiz')
1733
                ->setParameter('quiz', $quiz)
1734
                ->orderBy('rel.questionOrder', 'ASC')
1735
                ->getQuery()->getResult()
1736
            ;
1737
1738
            foreach ($rels as $rel) {
1739
                $qid = (int) $rel->getQuestion()->getIid();
1740
                $payload['question_ids'][] = $qid;
1741
                $payload['question_orders'][] = (int) $rel->getQuestionOrder();
1742
                $neededQuestionIds[$qid] = true;
1743
            }
1744
1745
            $legacyCourse->resources[RESOURCE_QUIZ][$iid] =
1746
                $this->mkLegacyItem(
1747
                    RESOURCE_QUIZ,
1748
                    $iid,
1749
                    $payload,
1750
                    ['question_ids', 'question_orders']
1751
                );
1752
        }
1753
1754
        error_log('COURSE_BUILD: build_quizzes done; total='.\count($quizzes));
1755
1756
        return array_keys($neededQuestionIds);
1757
    }
1758
1759
    /**
1760
     * Export Quiz Questions (answers and options promoted).
1761
     *
1762
     * @param array<int> $questionIds
1763
     */
1764
    public function build_quiz_questions(
1765
        object $legacyCourse,
1766
        ?CourseEntity $courseEntity,
1767
        ?SessionEntity $sessionEntity,
1768
        array $questionIds
1769
    ): void {
1770
        if (!$courseEntity) {
1771
            return;
1772
        }
1773
1774
        // use the common context builder so withBaseContent is applied consistently.
1775
        $qb = $this->createContextQb(
1776
            CQuizQuestion::class,
1777
            $courseEntity,
1778
            $sessionEntity,
1779
            'qq'
1780
        );
1781
1782
        if (!empty($questionIds)) {
1783
            $qb->andWhere('qq.iid IN (:ids)')
1784
                ->setParameter('ids', array_map('intval', $questionIds));
1785
        }
1786
1787
        /** @var CQuizQuestion[] $questions */
1788
        $questions = $qb->getQuery()->getResult();
1789
1790
        $this->exportQuestionsWithAnswers($legacyCourse, $questions);
1791
    }
1792
1793
    /**
1794
     * Internal exporter for quiz questions + answers (+ options for MATF type).
1795
     *
1796
     * @param array<int,CQuizQuestion> $questions
1797
     */
1798
    private function exportQuestionsWithAnswers(object $legacyCourse, array $questions): void
1799
    {
1800
        foreach ($questions as $q) {
1801
            $qid = (int) $q->getIid();
1802
1803
            $payload = [
1804
                'question' => (string) $q->getQuestion(),
1805
                'description' => (string) ($q->getDescription() ?? ''),
1806
                'ponderation' => (float) $q->getPonderation(),
1807
                'position' => (int) $q->getPosition(),
1808
                'type' => (int) $q->getType(),
1809
                'quiz_type' => (int) $q->getType(),
1810
                'picture' => (string) ($q->getPicture() ?? ''),
1811
                'level' => (int) $q->getLevel(),
1812
                'extra' => (string) ($q->getExtra() ?? ''),
1813
                'feedback' => (string) ($q->getFeedback() ?? ''),
1814
                'question_code' => (string) ($q->getQuestionCode() ?? ''),
1815
                'mandatory' => (int) $q->getMandatory(),
1816
                'duration' => $q->getDuration(),
1817
                'parent_media_id' => $q->getParentMediaId(),
1818
                'answers' => [],
1819
            ];
1820
1821
            $ans = $this->em->createQueryBuilder()
1822
                ->select('a')
1823
                ->from(CQuizAnswer::class, 'a')
1824
                ->andWhere('a.question = :q')->setParameter('q', $q)
1825
                ->orderBy('a.position', 'ASC')
1826
                ->getQuery()->getResult()
1827
            ;
1828
1829
            foreach ($ans as $a) {
1830
                $payload['answers'][] = [
1831
                    'id' => (int) $a->getIid(),
1832
                    'answer' => (string) $a->getAnswer(),
1833
                    'comment' => (string) ($a->getComment() ?? ''),
1834
                    'ponderation' => (float) $a->getPonderation(),
1835
                    'position' => (int) $a->getPosition(),
1836
                    'hotspot_coordinates' => $a->getHotspotCoordinates(),
1837
                    'hotspot_type' => $a->getHotspotType(),
1838
                    'correct' => $a->getCorrect(),
1839
                ];
1840
            }
1841
1842
            if (\defined('MULTIPLE_ANSWER_TRUE_FALSE') && MULTIPLE_ANSWER_TRUE_FALSE === (int) $q->getType()) {
1843
                $opts = $this->em->createQueryBuilder()
1844
                    ->select('o')
1845
                    ->from(CQuizQuestionOption::class, 'o')
1846
                    ->andWhere('o.question = :q')->setParameter('q', $q)
1847
                    ->orderBy('o.position', 'ASC')
1848
                    ->getQuery()->getResult()
1849
                ;
1850
1851
                $payload['question_options'] = array_map(static fn ($o) => [
1852
                    'id' => (int) $o->getIid(),
1853
                    'name' => (string) $o->getTitle(),
1854
                    'position' => (int) $o->getPosition(),
1855
                ], $opts);
1856
            }
1857
1858
            $legacyCourse->resources[RESOURCE_QUIZQUESTION][$qid] =
1859
                $this->mkLegacyItem(RESOURCE_QUIZQUESTION, $qid, $payload, ['answers', 'question_options']);
1860
1861
            error_log(
1862
                'COURSE_BUILD: QQ qid='.$qid.
1863
                ' answers='.\count($legacyCourse->resources[RESOURCE_QUIZQUESTION][$qid]->answers ?? [])
1864
            );
1865
        }
1866
    }
1867
1868
    /**
1869
     * Safe count helper for mixed values.
1870
     */
1871
    private function safeCount(mixed $v): int
0 ignored issues
show
Unused Code introduced by
The method safeCount() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
1872
    {
1873
        return (\is_array($v) || $v instanceof Countable) ? \count($v) : 0;
1874
    }
1875
1876
    /**
1877
     * Export Link category as legacy item.
1878
     */
1879
    public function build_link_category(CLinkCategory $category): void
1880
    {
1881
        $id = (int) $category->getIid();
1882
        if ($id <= 0) {
1883
            return;
1884
        }
1885
1886
        $payload = [
1887
            'title' => (string) $category->getTitle(),
1888
            'description' => (string) ($category->getDescription() ?? ''),
1889
            'category_title' => (string) $category->getTitle(),
1890
        ];
1891
1892
        $this->course->resources[RESOURCE_LINKCATEGORY][$id] =
1893
            $this->mkLegacyItem(RESOURCE_LINKCATEGORY, $id, $payload);
1894
    }
1895
1896
    /**
1897
     * Export Links (and their categories once).
1898
     *
1899
     * @param array<int> $ids
1900
     */
1901
    public function build_links(
1902
        object $legacyCourse,
1903
        ?CourseEntity $courseEntity,
1904
        ?SessionEntity $sessionEntity,
1905
        array $ids
1906
    ): void {
1907
        if (!$courseEntity instanceof CourseEntity) {
1908
            return;
1909
        }
1910
1911
        $qb = $this->createContextQb(
1912
            CLink::class,
1913
            $courseEntity,
1914
            $sessionEntity,
1915
            'l'
1916
        );
1917
1918
        if (!empty($ids)) {
1919
            $qb->andWhere('l.iid IN (:ids)')
1920
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))));
1921
        }
1922
1923
        /** @var CLink[] $links */
1924
        $links = $qb->getQuery()->getResult();
1925
1926
        $exportedCats = [];
1927
1928
        foreach ($links as $link) {
1929
            $iid = (int) $link->getIid();
1930
            $title = (string) $link->getTitle();
1931
            $url = (string) $link->getUrl();
1932
            $desc = (string) ($link->getDescription() ?? '');
1933
            $tgt = (string) ($link->getTarget() ?? '');
1934
1935
            $cat = $link->getCategory();
1936
            $catId = (int) ($cat?->getIid() ?? 0);
1937
1938
            if ($catId > 0 && !isset($exportedCats[$catId])) {
1939
                $this->build_link_category($cat);
1940
                $exportedCats[$catId] = true;
1941
            }
1942
1943
            $payload = [
1944
                'title' => '' !== $title ? $title : $url,
1945
                'url' => $url,
1946
                'description' => $desc,
1947
                'target' => $tgt,
1948
                'category_id' => $catId,
1949
                'on_homepage' => false,
1950
            ];
1951
1952
            $legacyCourse->resources[RESOURCE_LINK][$iid] =
1953
                $this->mkLegacyItem(RESOURCE_LINK, $iid, $payload);
1954
        }
1955
    }
1956
1957
    /**
1958
     * Format DateTime as string "Y-m-d H:i:s".
1959
     */
1960
    private function fmtDate(?DateTimeInterface $dt): string
1961
    {
1962
        return $dt ? $dt->format('Y-m-d H:i:s') : '';
1963
    }
1964
1965
    /**
1966
     * Create a legacy item object, promoting selected array keys to top-level.
1967
     *
1968
     * @param array<int,string> $arrayKeysToPromote
1969
     */
1970
    private function mkLegacyItem(string $type, int $sourceId, array|object $obj, array $arrayKeysToPromote = []): stdClass
1971
    {
1972
        $o = new stdClass();
1973
        $o->type = $type;
1974
        $o->source_id = $sourceId;
1975
        $o->destination_id = -1;
1976
        $o->has_obj = true;
1977
        $o->obj = (object) $obj;
1978
1979
        if (!isset($o->obj->iid)) {
1980
            $o->obj->iid = $sourceId;
1981
        }
1982
        if (!isset($o->id)) {
1983
            $o->id = $sourceId;
1984
        }
1985
        if (!isset($o->obj->id)) {
1986
            $o->obj->id = $sourceId;
1987
        }
1988
1989
        foreach ((array) $obj as $k => $v) {
1990
            if (\is_scalar($v) || null === $v) {
1991
                if (!property_exists($o, $k)) {
1992
                    $o->{$k} = $v;
1993
                }
1994
            }
1995
        }
1996
1997
        $objArr = (array) $obj;
1998
        foreach ($arrayKeysToPromote as $k) {
1999
            if (isset($objArr[$k]) && \is_array($objArr[$k])) {
2000
                $o->{$k} = $objArr[$k];
2001
            }
2002
        }
2003
2004
        if (RESOURCE_DOCUMENT === $type) {
2005
            $o->path = (string) ($o->path ?? $o->full_path ?? $o->obj->path ?? $o->obj->full_path ?? '');
2006
            $o->full_path = (string) ($o->full_path ?? $o->path ?? $o->obj->full_path ?? $o->obj->path ?? '');
2007
            $o->file_type = (string) ($o->file_type ?? $o->filetype ?? $o->obj->file_type ?? $o->obj->filetype ?? '');
2008
            $o->filetype = (string) ($o->filetype ?? $o->file_type ?? $o->obj->filetype ?? $o->obj->file_type ?? '');
2009
            $o->title = (string) ($o->title ?? $o->obj->title ?? '');
2010
            if (!isset($o->name) || '' === $o->name || null === $o->name) {
2011
                $o->name = '' !== $o->title ? $o->title : ('document '.$sourceId);
2012
            }
2013
        }
2014
2015
        if (RESOURCE_SURVEYQUESTION === $type) {
2016
            if (!isset($o->survey_question_type) && isset($o->type)) {
2017
                $o->survey_question_type = $o->type;
2018
            }
2019
            if (!isset($o->type) && isset($o->survey_question_type)) {
2020
                $o->type = $o->survey_question_type;
2021
            }
2022
2023
            if (isset($o->obj) && \is_object($o->obj)) {
2024
                if (!isset($o->obj->survey_question_type) && isset($o->obj->type)) {
2025
                    $o->obj->survey_question_type = $o->obj->type;
2026
                }
2027
                if (!isset($o->obj->type) && isset($o->obj->survey_question_type)) {
2028
                    $o->obj->type = $o->obj->survey_question_type;
2029
                }
2030
            }
2031
        }
2032
2033
        if (!isset($o->name) || '' === $o->name || null === $o->name) {
2034
            if (isset($objArr['name']) && '' !== (string) $objArr['name']) {
2035
                $o->name = (string) $objArr['name'];
2036
            } elseif (isset($objArr['title']) && '' !== (string) $objArr['title']) {
2037
                $o->name = (string) $objArr['title'];
2038
            } else {
2039
                $o->name = $type.' '.$sourceId;
2040
            }
2041
        }
2042
2043
        return $o;
2044
    }
2045
2046
    /**
2047
     * Build an id filter closure.
2048
     *
2049
     * @param array<int> $idsFilter
2050
     *
2051
     * @return Closure(int):bool
2052
     */
2053
    private function makeIdFilter(array $idsFilter): Closure
2054
    {
2055
        if (empty($idsFilter)) {
2056
            return static fn (int $id): bool => true;
2057
        }
2058
        $set = array_fill_keys(array_map('intval', $idsFilter), true);
2059
2060
        return static fn (int $id): bool => isset($set[$id]);
2061
    }
2062
2063
    /**
2064
     * Export Tool intro only for the course_homepage tool.
2065
     * Prefers the session-specific intro when both (session and base) exist.
2066
     */
2067
    public function build_tool_intro(
2068
        object $legacyCourse,
2069
        ?CourseEntity $courseEntity,
2070
        ?SessionEntity $sessionEntity
2071
    ): void {
2072
        if (!$courseEntity instanceof CourseEntity) {
2073
            return;
2074
        }
2075
2076
        $qb = $this->createContextQb(
2077
            CToolIntro::class,
2078
            $courseEntity,
2079
            $sessionEntity,
2080
            'ti'
2081
        );
2082
2083
        $linksAlias = $this->getContextLinksAlias('ti');
2084
2085
        $qb->innerJoin('ti.courseTool', 'ct')
2086
            ->andWhere('ct.title = :title')
2087
            ->setParameter('title', 'course_homepage');
2088
2089
        if ($sessionEntity) {
2090
            $qb->andWhere('(ct.session = :session OR ct.session IS NULL)');
2091
        } else {
2092
            $qb->andWhere('ct.session IS NULL');
2093
        }
2094
2095
        if ($sessionEntity) {
2096
            $qb->addOrderBy('CASE WHEN '.$linksAlias.'.session IS NULL THEN 0 ELSE 1 END', 'DESC');
2097
        }
2098
2099
        $qb->addOrderBy('ti.iid', 'ASC');
2100
2101
        /** @var CToolIntro[] $rows */
2102
        $rows = $qb->getQuery()->getResult();
2103
        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...
2104
            return;
2105
        }
2106
2107
        $selected = $rows[0];
2108
2109
        $ctool = $selected->getCourseTool();
2110
        $titleKey = (string) ($ctool?->getTitle() ?? 'course_homepage');
2111
        if ('' === $titleKey) {
2112
            $titleKey = 'course_homepage';
2113
        }
2114
2115
        $payload = [
2116
            'id' => $titleKey,
2117
            'intro_text' => (string) $selected->getIntroText(),
2118
        ];
2119
2120
        $legacyCourse->resources[RESOURCE_TOOL_INTRO][$titleKey] =
2121
            $this->mkLegacyItem(RESOURCE_TOOL_INTRO, 0, $payload);
2122
    }
2123
2124
    /**
2125
     * Export Forum categories.
2126
     *
2127
     * @param array<int> $ids
2128
     */
2129
    public function build_forum_category(
2130
        object $legacyCourse,
2131
        ?CourseEntity $courseEntity,
2132
        ?SessionEntity $sessionEntity,
2133
        array $ids
2134
    ): void {
2135
        if (!$courseEntity instanceof CourseEntity) {
2136
            return;
2137
        }
2138
2139
        $repo = Container::getForumCategoryRepository();
2140
        $qb = $this->getResourcesByCourseQbFromRepo($repo, $courseEntity, $sessionEntity, $this->withBaseContent);
2141
2142
        /** @var CForumCategory[] $categories */
2143
        $categories = $qb->getQuery()->getResult();
2144
2145
        $keep = $this->makeIdFilter($ids);
2146
2147
        foreach ($categories as $cat) {
2148
            $id = (int) $cat->getIid();
2149
            if (!$keep($id)) {
2150
                continue;
2151
            }
2152
2153
            $payload = [
2154
                'title' => (string) $cat->getTitle(),
2155
                'description' => (string) ($cat->getCatComment() ?? ''),
2156
                'cat_title' => (string) $cat->getTitle(),
2157
                'cat_comment' => (string) ($cat->getCatComment() ?? ''),
2158
            ];
2159
2160
            $legacyCourse->resources[RESOURCE_FORUMCATEGORY][$id] =
2161
                $this->mkLegacyItem(RESOURCE_FORUMCATEGORY, $id, $payload);
2162
        }
2163
    }
2164
2165
    /**
2166
     * Export Forums.
2167
     *
2168
     * @param array<int> $ids
2169
     */
2170
    public function build_forums(
2171
        object $legacyCourse,
2172
        ?CourseEntity $courseEntity,
2173
        ?SessionEntity $sessionEntity,
2174
        array $ids
2175
    ): void {
2176
        if (!$courseEntity instanceof CourseEntity) {
2177
            return;
2178
        }
2179
2180
        $qb = $this->createContextQb(
2181
            CForum::class,
2182
            $courseEntity,
2183
            $sessionEntity,
2184
            'f'
2185
        );
2186
2187
        if (!empty($ids)) {
2188
            $qb->andWhere('f.iid IN (:ids)')
2189
                ->setParameter('ids', array_values(array_unique(array_map('intval', $ids))));
2190
        }
2191
2192
        /** @var CForum[] $forums */
2193
        $forums = $qb->getQuery()->getResult();
2194
2195
        $keep = $this->makeIdFilter($ids);
2196
2197
        foreach ($forums as $f) {
2198
            $id = (int) $f->getIid();
2199
            if (!$keep($id)) {
2200
                continue;
2201
            }
2202
2203
            $payload = [
2204
                'title' => (string) $f->getTitle(),
2205
                'description' => (string) ($f->getForumComment() ?? ''),
2206
                'forum_title' => (string) $f->getTitle(),
2207
                'forum_comment' => (string) ($f->getForumComment() ?? ''),
2208
                'forum_category' => (int) ($f->getForumCategory()?->getIid() ?? 0),
2209
                'allow_anonymous' => (int) ($f->getAllowAnonymous() ?? 0),
2210
                'allow_edit' => (int) ($f->getAllowEdit() ?? 0),
2211
                'approval_direct_post' => (string) ($f->getApprovalDirectPost() ?? '0'),
2212
                'allow_attachments' => (int) ($f->getAllowAttachments() ?? 1),
2213
                'allow_new_threads' => (int) ($f->getAllowNewThreads() ?? 1),
2214
                'default_view' => (string) ($f->getDefaultView() ?? 'flat'),
2215
                'forum_of_group' => (string) ($f->getForumOfGroup() ?? '0'),
2216
                'forum_group_public_private' => (string) ($f->getForumGroupPublicPrivate() ?? 'public'),
2217
                'moderated' => (int) ($f->isModerated() ? 1 : 0),
2218
                'start_time' => $this->fmtDate($f->getStartTime()),
2219
                'end_time' => $this->fmtDate($f->getEndTime()),
2220
            ];
2221
2222
            $legacyCourse->resources[RESOURCE_FORUM][$id] =
2223
                $this->mkLegacyItem(RESOURCE_FORUM, $id, $payload);
2224
        }
2225
    }
2226
2227
    /**
2228
     * Export Forum threads.
2229
     *
2230
     * @param array<int> $ids
2231
     */
2232
    public function build_forum_topics(
2233
        object $legacyCourse,
2234
        ?CourseEntity $courseEntity,
2235
        ?SessionEntity $sessionEntity,
2236
        array $ids
2237
    ): void {
2238
        if (!$courseEntity instanceof CourseEntity) {
2239
            return;
2240
        }
2241
2242
        $repo = Container::getForumThreadRepository();
2243
        $qb = $this->getResourcesByCourseQbFromRepo($repo, $courseEntity, $sessionEntity, $this->withBaseContent);
2244
2245
        /** @var CForumThread[] $threads */
2246
        $threads = $qb->getQuery()->getResult();
2247
2248
        $keep = $this->makeIdFilter($ids);
2249
2250
        foreach ($threads as $t) {
2251
            $id = (int) $t->getIid();
2252
            if (!$keep($id)) {
2253
                continue;
2254
            }
2255
2256
            $payload = [
2257
                'title' => (string) $t->getTitle(),
2258
                'thread_title' => (string) $t->getTitle(),
2259
                'title_qualify' => (string) ($t->getThreadTitleQualify() ?? ''),
2260
                'topic_poster_name' => (string) ($t->getUser()?->getUsername() ?? ''),
2261
                'forum_id' => (int) ($t->getForum()?->getIid() ?? 0),
2262
                'thread_date' => $this->fmtDate($t->getThreadDate()),
2263
                'thread_sticky' => (int) ($t->getThreadSticky() ? 1 : 0),
2264
                'thread_title_qualify' => (string) ($t->getThreadTitleQualify() ?? ''),
2265
                'thread_qualify_max' => (float) $t->getThreadQualifyMax(),
2266
                'thread_weight' => (float) $t->getThreadWeight(),
2267
                'thread_peer_qualify' => (int) ($t->isThreadPeerQualify() ? 1 : 0),
2268
            ];
2269
2270
            $legacyCourse->resources[RESOURCE_FORUMTOPIC][$id] =
2271
                $this->mkLegacyItem(RESOURCE_FORUMTOPIC, $id, $payload);
2272
        }
2273
    }
2274
2275
    /**
2276
     * Export first post for each thread as topic root post.
2277
     *
2278
     * @param array<int> $ids
2279
     */
2280
    public function build_forum_posts(
2281
        object $legacyCourse,
2282
        ?CourseEntity $courseEntity,
2283
        ?SessionEntity $sessionEntity,
2284
        array $ids
2285
    ): void {
2286
        if (!$courseEntity instanceof CourseEntity) {
2287
            return;
2288
        }
2289
2290
        $repoThread = Container::getForumThreadRepository();
2291
        $repoPost = Container::getForumPostRepository();
2292
        $qb = $this->getResourcesByCourseQbFromRepo($repoThread, $courseEntity, $sessionEntity, $this->withBaseContent);
2293
2294
        /** @var CForumThread[] $threads */
2295
        $threads = $qb->getQuery()->getResult();
2296
2297
        $keep = $this->makeIdFilter($ids);
2298
2299
        foreach ($threads as $t) {
2300
            $threadId = (int) $t->getIid();
2301
            if (!$keep($threadId)) {
2302
                continue;
2303
            }
2304
2305
            $first = $repoPost->findOneBy(['thread' => $t], ['postDate' => 'ASC', 'iid' => 'ASC']);
2306
            if (!$first) {
2307
                continue;
2308
            }
2309
2310
            $postId = (int) $first->getIid();
2311
            $titleFromPost = trim((string) $first->getTitle());
2312
            if ('' === $titleFromPost) {
2313
                $plain = trim(strip_tags((string) ($first->getPostText() ?? '')));
2314
                $titleFromPost = mb_substr('' !== $plain ? $plain : 'Post', 0, 60);
2315
            }
2316
2317
            $payload = [
2318
                'title' => $titleFromPost,
2319
                'post_title' => $titleFromPost,
2320
                'post_text' => (string) ($first->getPostText() ?? ''),
2321
                'thread_id' => $threadId,
2322
                'forum_id' => (int) ($t->getForum()?->getIid() ?? 0),
2323
                'post_notification' => (int) ($first->getPostNotification() ? 1 : 0),
2324
                'visible' => (int) ($first->getVisible() ? 1 : 0),
2325
                'status' => (int) ($first->getStatus() ?? CForumPost::STATUS_VALIDATED),
2326
                'post_parent_id' => (int) ($first->getPostParent()?->getIid() ?? 0),
2327
                'poster_id' => (int) ($first->getUser()?->getId() ?? 0),
2328
                'text' => (string) ($first->getPostText() ?? ''),
2329
                'poster_name' => (string) ($first->getUser()?->getUsername() ?? ''),
2330
                'post_date' => $this->fmtDate($first->getPostDate()),
2331
            ];
2332
2333
            $legacyCourse->resources[RESOURCE_FORUMPOST][$postId] =
2334
                $this->mkLegacyItem(RESOURCE_FORUMPOST, $postId, $payload);
2335
        }
2336
    }
2337
2338
    /**
2339
     * New Chamilo 2 build: CDocumentRepository-based (instead of legacy tables).
2340
     *
2341
     * @param array<int> $idList
2342
     */
2343
    private function build_documents_with_repo(
2344
        ?CourseEntity $course,
2345
        ?SessionEntity $session,
2346
        bool $withBaseContent,
2347
        array $idList = []
2348
    ): void {
2349
        if (!$course instanceof CourseEntity) {
2350
            return;
2351
        }
2352
2353
        $qb = $this->getResourcesByCourseQbFromRepo($this->docRepo, $course, $session, $withBaseContent);
2354
2355
        if (!empty($idList)) {
2356
            $qb->andWhere('resource.iid IN (:ids)')
2357
                ->setParameter('ids', array_values(array_unique(array_map('intval', $idList))));
2358
        }
2359
2360
        /** @var CDocument[] $docs */
2361
        $docs = $qb->getQuery()->getResult();
2362
2363
        $documentsRoot = $this->docRepo->getCourseDocumentsRootNode($course);
2364
2365
        foreach ($docs as $doc) {
2366
            $node     = $doc->getResourceNode();
2367
            $filetype = $doc->getFiletype(); // 'file' | 'folder' | ...
2368
            $title    = $doc->getTitle();
2369
            $comment  = $doc->getComment() ?? '';
2370
            $iid      = (int) $doc->getIid();
2371
2372
            $size = 0;
2373
            if ('folder' === $filetype) {
2374
                $size = $this->docRepo->getFolderSize($node, $course, $session);
2375
            } else {
2376
                $files = $node?->getResourceFiles();
2377
                if ($files && $files->count() > 0) {
2378
                    /** @var ResourceFile $first */
2379
                    $first = $files->first();
2380
                    $size  = (int) $first->getSize();
2381
                }
2382
            }
2383
2384
            $rel = '';
2385
            if ($node instanceof ResourceNode) {
2386
                if ($documentsRoot instanceof ResourceNode) {
2387
                    $rel = (string) $node->getPathForDisplayRemoveBase((string) $documentsRoot->getPath());
2388
                } else {
2389
                    $rel = (string) $node->convertPathForDisplay((string) $node->getPath());
2390
                    $rel = preg_replace('~^/?Documents/?~i', '', (string) $rel) ?? $rel;
2391
                }
2392
            }
2393
            $rel = trim((string) $rel, '/');
2394
            $pathForSelector = 'document' . ($rel !== '' ? '/'.$rel : '');
2395
            if ('folder' === $filetype) {
2396
                $pathForSelector = rtrim($pathForSelector, '/').'/';
2397
            }
2398
2399
            $exportDoc = new Document(
2400
                $iid,
2401
                $pathForSelector,
2402
                $comment,
2403
                $title,
2404
                $filetype,
2405
                (string) $size
2406
            );
2407
2408
            $this->course->add_resource($exportDoc);
2409
        }
2410
    }
2411
2412
    /**
2413
     * Backward-compatible wrapper for build_documents_with_repo().
2414
     *
2415
     * @param array<int> $idList
2416
     */
2417
    public function build_documents(
2418
        int $session_id = 0,
2419
        int $courseId = 0,
2420
        bool $withBaseContent = false,
2421
        array $idList = []
2422
    ): void {
2423
        /** @var CourseEntity|null $course */
2424
        $course = $this->em->getRepository(CourseEntity::class)->find($courseId);
2425
2426
        /** @var SessionEntity|null $session */
2427
        $session = $session_id ? $this->em->getRepository(SessionEntity::class)->find($session_id) : null;
2428
2429
        $this->build_documents_with_repo($course, $session, $withBaseContent, $idList);
2430
    }
2431
2432
    /**
2433
     * Create a QueryBuilder pre-configured with course/session visibility constraints
2434
     * based on ResourceNode/ResourceLink associations.
2435
     *
2436
     * @param class-string $fromClass
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
2437
     */
2438
    private function createContextQb(
2439
        string $fromClass,
2440
        CourseEntity $courseEntity,
2441
        ?SessionEntity $sessionEntity,
2442
        string $alias = 'resource'
2443
    ): QueryBuilder {
2444
        $qb = $this->em->createQueryBuilder()
2445
            ->select($alias)
2446
            ->from($fromClass, $alias);
2447
2448
        $this->applyContextToQb($qb, $alias, $courseEntity, $sessionEntity);
2449
2450
        $qb->distinct();
2451
2452
        return $qb;
2453
    }
2454
2455
    /**
2456
     * Apply course/session constraints on an existing QueryBuilder.
2457
     *
2458
     * The method ensures the required joins exist with predictable aliases:
2459
     *  - "{$rootAlias}Node"  for ResourceNode
2460
     *  - "{$rootAlias}Links" for ResourceLink
2461
     *
2462
     * It returns the ResourceLink alias so callers can reference it (ORDER BY, etc).
2463
     */
2464
    private function applyContextToQb(
2465
        QueryBuilder $qb,
2466
        string $rootAlias,
2467
        CourseEntity $courseEntity,
2468
        ?SessionEntity $sessionEntity
2469
    ): string {
2470
        $nodeAlias = $this->getContextNodeAlias($rootAlias);
2471
        $linksAlias = $this->getContextLinksAlias($rootAlias);
2472
2473
        // Ensure ResourceNode join exists.
2474
        if (!$this->hasJoinAlias($qb, $nodeAlias)) {
2475
            $qb->innerJoin($rootAlias.'.resourceNode', $nodeAlias);
2476
        }
2477
2478
        // Ensure ResourceLinks join exists.
2479
        if (!$this->hasJoinAlias($qb, $linksAlias)) {
2480
            $qb->innerJoin($nodeAlias.'.resourceLinks', $linksAlias);
2481
        }
2482
2483
        // Base constraints.
2484
        $qb
2485
            ->andWhere($linksAlias.'.course = :course')
2486
            ->setParameter('course', $courseEntity)
2487
            ->andWhere($linksAlias.'.deletedAt IS NULL')
2488
            ->andWhere($linksAlias.'.endVisibilityAt IS NULL')
2489
        ;
2490
2491
        // Group constraint: avoid failing if "group" is mapped as an association.
2492
        $this->addNoGroupConstraint($qb, $linksAlias);
2493
2494
        // Session constraints controlled by copy option.
2495
        if (null !== $sessionEntity) {
2496
            $qb->setParameter('session', $sessionEntity);
2497
2498
            if ($this->withBaseContent) {
2499
                // Include both base course content + session content.
2500
                $qb->andWhere('('.$linksAlias.'.session IS NULL OR '.$linksAlias.'.session = :session)');
2501
            } else {
2502
                // Session-only.
2503
                $qb->andWhere($linksAlias.'.session = :session');
2504
            }
2505
        } else {
2506
            // Base-only when no session.
2507
            $qb->andWhere($linksAlias.'.session IS NULL');
2508
        }
2509
2510
        return $linksAlias;
2511
    }
2512
2513
    private function getContextNodeAlias(string $rootAlias): string
2514
    {
2515
        return $rootAlias.'Node';
2516
    }
2517
2518
    private function getContextLinksAlias(string $rootAlias): string
2519
    {
2520
        return $rootAlias.'Links';
2521
    }
2522
2523
    /**
2524
     * Detect whether a QB already contains a join alias.
2525
     */
2526
    private function hasJoinAlias(QueryBuilder $qb, string $alias): bool
2527
    {
2528
        $joins = $qb->getDQLPart('join');
2529
        if (!\is_array($joins)) {
2530
            return false;
2531
        }
2532
2533
        foreach ($joins as $group) {
2534
            if (!\is_array($group)) {
2535
                continue;
2536
            }
2537
            foreach ($group as $j) {
2538
                if (method_exists($j, 'getAlias') && $j->getAlias() === $alias) {
2539
                    return true;
2540
                }
2541
            }
2542
        }
2543
2544
        return false;
2545
    }
2546
2547
    /**
2548
     * Add "no group" constraint without breaking when ResourceLink::group is an association.
2549
     */
2550
    private function addNoGroupConstraint(QueryBuilder $qb, string $linksAlias): void
2551
    {
2552
        try {
2553
            $meta = $this->em->getClassMetadata(ResourceLink::class);
2554
            if ($meta->hasAssociation('group')) {
2555
                // Association: only NULL is safe here.
2556
                $qb->andWhere($qb->expr()->isNull($linksAlias.'.group'));
2557
                return;
2558
            }
2559
        } catch (Throwable) {
2560
            // Fall back to the legacy condition below.
2561
        }
2562
2563
        // Scalar mapping (or unknown): keep legacy behavior.
2564
        $qb->andWhere(
2565
            $qb->expr()->orX(
2566
                $qb->expr()->isNull($linksAlias.'.group'),
2567
                $qb->expr()->eq($linksAlias.'.group', 0)
2568
            )
2569
        );
2570
    }
2571
2572
    /**
2573
     * Build a QueryBuilder from a repository implementing getResourcesByCourse(),
2574
     * passing $withBaseContent
2575
     */
2576
    private function getResourcesByCourseQbFromRepo(
2577
        object $repo,
2578
        CourseEntity $course,
2579
        ?SessionEntity $session,
2580
        bool $withBaseContent
2581
    ): QueryBuilder {
2582
        if (!method_exists($repo, 'getResourcesByCourse')) {
2583
            throw new \RuntimeException('Repository does not implement getResourcesByCourse().');
2584
        }
2585
2586
        try {
2587
            /** @var QueryBuilder $qb */
2588
            $qb = $repo->getResourcesByCourse(
2589
                $course,
2590
                $session,
2591
                null,               // group
2592
                null,               // parentNode
2593
                true,               // displayOnlyPublished
2594
                false,              // displayOrder
2595
                $withBaseContent    // withBaseContentOverride
2596
            );
2597
2598
            return $qb;
2599
        } catch (Throwable $e) {
2600
            throw new \RuntimeException(
2601
                'Failed to call getResourcesByCourse() with withBaseContentOverride as 7th parameter.',
2602
                0,
2603
                $e
2604
            );
2605
        }
2606
    }
2607
}
2608