Passed
Pull Request — master (#7027)
by
unknown
12:49 queued 03:07
created

CourseExport::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 10
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CourseBundle\Component\CourseCopy\Moodle\Builder;
8
9
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\QuizExport;
10
use Chamilo\CourseBundle\Component\CourseCopy\Resources\CalendarEvent;
11
use Exception;
12
13
use const PHP_EOL;
14
use const ENT_QUOTES;
15
use const ENT_SUBSTITUTE;
16
17
/**
18
 * Writes the course-level directory and XMLs inside the export root.
19
 */
20
class CourseExport
21
{
22
    /**
23
     * @var object
24
     */
25
    private $course;
26
27
    /**
28
     * @var array<string,mixed>
29
     */
30
    private array $courseInfo;
31
32
    /**
33
     * @var array<int,array<string,mixed>>
34
     */
35
    private array $activities;
36
37
    /**
38
     * @param array<int,array<string,mixed>>|null $activities
39
     *
40
     * @throws Exception
41
     */
42
    public function __construct(object $course, ?array $activities = [])
43
    {
44
        $this->course = $course;
45
        $this->courseInfo = (array) (api_get_course_info($course->code) ?? []);
46
47
        if (empty($this->courseInfo)) {
48
            throw new Exception('Course not found.');
49
        }
50
51
        $this->activities = $activities ?? [];
52
    }
53
54
    /**
55
     * Export the course-related files to the appropriate directory.
56
     */
57
    public function exportCourse(string $exportDir): void
58
    {
59
        $courseDir = $exportDir.'/course';
60
        if (!is_dir($courseDir)) {
61
            mkdir($courseDir, api_get_permissions_for_new_directories(), true);
62
        }
63
64
        $this->createCourseXml($courseDir);
65
        $this->createEnrolmentsXml($this->courseInfo['enrolments'] ?? [], $courseDir);
66
        $this->createInforefXml($courseDir);
67
        $this->createRolesXml($this->courseInfo['roles'] ?? [], $courseDir);
68
        $this->createCalendarXml($this->courseInfo['calendar'] ?? [], $courseDir);
69
        $this->createCommentsXml($this->courseInfo['comments'] ?? [], $courseDir);
70
        $this->createCompetenciesXml($this->courseInfo['competencies'] ?? [], $courseDir);
71
        $this->createCompletionDefaultsXml($this->courseInfo['completiondefaults'] ?? [], $courseDir);
72
        $this->createContentBankXml($this->courseInfo['contentbank'] ?? [], $courseDir);
73
        $this->createFiltersXml($this->courseInfo['filters'] ?? [], $courseDir);
74
    }
75
76
    /**
77
     * Create course.xml based on the course data.
78
     */
79
    private function createCourseXml(string $destinationDir): void
80
    {
81
        $courseId = (int) ($this->courseInfo['real_id'] ?? 0);
82
        $contextId = (int) ($this->courseInfo['real_id'] ?? 1);
83
        $shortname = (string) ($this->courseInfo['code'] ?? 'Unknown Course');
84
        $fullname = (string) ($this->courseInfo['title'] ?? 'Unknown Fullname');
85
        $showgrades = (int) ($this->courseInfo['showgrades'] ?? 0);
86
        $startdate = (int) ($this->courseInfo['startdate'] ?? time());
87
        $enddate = (int) ($this->courseInfo['enddate'] ?? (time() + 31536000));
88
        $visible = (int) ($this->courseInfo['visible'] ?? 1);
89
        $enablecompletion = (int) ($this->courseInfo['enablecompletion'] ?? 0);
90
91
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
92
        $xmlContent .= '<course id="'.$courseId.'" contextid="'.$contextId.'">'.PHP_EOL;
93
        $xmlContent .= '  <shortname>'.htmlspecialchars($shortname).'</shortname>'.PHP_EOL;
94
        $xmlContent .= '  <fullname>'.htmlspecialchars($fullname).'</fullname>'.PHP_EOL;
95
        $xmlContent .= '  <idnumber></idnumber>'.PHP_EOL;
96
        $xmlContent .= '  <summary></summary>'.PHP_EOL;
97
        $xmlContent .= '  <summaryformat>1</summaryformat>'.PHP_EOL;
98
        $xmlContent .= '  <format>topics</format>'.PHP_EOL;
99
        $xmlContent .= '  <showgrades>'.$showgrades.'</showgrades>'.PHP_EOL;
100
        $xmlContent .= '  <newsitems>5</newsitems>'.PHP_EOL;
101
        $xmlContent .= '  <startdate>'.$startdate.'</startdate>'.PHP_EOL;
102
        $xmlContent .= '  <enddate>'.$enddate.'</enddate>'.PHP_EOL;
103
        $xmlContent .= '  <marker>0</marker>'.PHP_EOL;
104
        $xmlContent .= '  <maxbytes>0</maxbytes>'.PHP_EOL;
105
        $xmlContent .= '  <legacyfiles>0</legacyfiles>'.PHP_EOL;
106
        $xmlContent .= '  <showreports>0</showreports>'.PHP_EOL;
107
        $xmlContent .= '  <visible>'.$visible.'</visible>'.PHP_EOL;
108
        $xmlContent .= '  <groupmode>0</groupmode>'.PHP_EOL;
109
        $xmlContent .= '  <groupmodeforce>0</groupmodeforce>'.PHP_EOL;
110
        $xmlContent .= '  <defaultgroupingid>0</defaultgroupingid>'.PHP_EOL;
111
        $xmlContent .= '  <lang></lang>'.PHP_EOL;
112
        $xmlContent .= '  <theme></theme>'.PHP_EOL;
113
        $xmlContent .= '  <timecreated>'.time().'</timecreated>'.PHP_EOL;
114
        $xmlContent .= '  <timemodified>'.time().'</timemodified>'.PHP_EOL;
115
        $xmlContent .= '  <requested>0</requested>'.PHP_EOL;
116
        $xmlContent .= '  <showactivitydates>1</showactivitydates>'.PHP_EOL;
117
        $xmlContent .= '  <showcompletionconditions>1</showcompletionconditions>'.PHP_EOL;
118
        $xmlContent .= '  <enablecompletion>'.$enablecompletion.'</enablecompletion>'.PHP_EOL;
119
        $xmlContent .= '  <completionnotify>0</completionnotify>'.PHP_EOL;
120
        $xmlContent .= '  <category id="1">'.PHP_EOL;
121
        $xmlContent .= '    <name>Miscellaneous</name>'.PHP_EOL;
122
        $xmlContent .= '    <description>$@NULL@$</description>'.PHP_EOL;
123
        $xmlContent .= '  </category>'.PHP_EOL;
124
        $xmlContent .= '  <tags>'.PHP_EOL;
125
        $xmlContent .= '  </tags>'.PHP_EOL;
126
        $xmlContent .= '  <customfields>'.PHP_EOL;
127
        $xmlContent .= '  </customfields>'.PHP_EOL;
128
        $xmlContent .= '  <courseformatoptions>'.PHP_EOL;
129
        $xmlContent .= '    <courseformatoption>'.PHP_EOL;
130
        $xmlContent .= '      <format>topics</format>'.PHP_EOL;
131
        $xmlContent .= '      <sectionid>0</sectionid>'.PHP_EOL;
132
        $xmlContent .= '      <name>hiddensections</name>'.PHP_EOL;
133
        $xmlContent .= '      <value>0</value>'.PHP_EOL;
134
        $xmlContent .= '    </courseformatoption>'.PHP_EOL;
135
        $xmlContent .= '    <courseformatoption>'.PHP_EOL;
136
        $xmlContent .= '      <format>topics</format>'.PHP_EOL;
137
        $xmlContent .= '      <sectionid>0</sectionid>'.PHP_EOL;
138
        $xmlContent .= '      <name>coursedisplay</name>'.PHP_EOL;
139
        $xmlContent .= '      <value>0</value>'.PHP_EOL;
140
        $xmlContent .= '    </courseformatoption>'.PHP_EOL;
141
        $xmlContent .= '  </courseformatoptions>'.PHP_EOL;
142
        $xmlContent .= '</course>';
143
144
        file_put_contents($destinationDir.'/course.xml', $xmlContent);
145
    }
146
147
    /**
148
     * Create enrolments.xml based on the course data.
149
     *
150
     * @param array<int,array<string,mixed>> $enrolmentsData
151
     */
152
    private function createEnrolmentsXml(array $enrolmentsData, string $destinationDir): void
153
    {
154
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
155
        $xmlContent .= '<enrolments>'.PHP_EOL;
156
        foreach ($enrolmentsData as $enrol) {
157
            $id = (int) ($enrol['id'] ?? 0);
158
            $type = (string) ($enrol['type'] ?? 'manual');
159
            $status = (int) ($enrol['status'] ?? 1);
160
161
            $xmlContent .= '  <enrol id="'.$id.'">'.PHP_EOL;
162
            $xmlContent .= '    <enrol>'.htmlspecialchars($type).'</enrol>'.PHP_EOL;
163
            $xmlContent .= '    <status>'.$status.'</status>'.PHP_EOL;
164
            $xmlContent .= '  </enrol>'.PHP_EOL;
165
        }
166
        $xmlContent .= '</enrolments>';
167
168
        file_put_contents($destinationDir.'/enrolments.xml', $xmlContent);
169
    }
170
171
    /**
172
     * Creates the inforef.xml file with question category references and a basic role ref.
173
     */
174
    private function createInforefXml(string $destinationDir): void
175
    {
176
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
177
        $xmlContent .= '<inforef>'.PHP_EOL;
178
179
        // Gather unique question category ids from quiz activities
180
        $questionCategories = [];
181
        foreach ($this->activities as $activity) {
182
            if (($activity['modulename'] ?? '') === 'quiz') {
183
                $quizExport = new QuizExport($this->course);
184
                $quizData = $quizExport->getData((int) $activity['id'], (int) $activity['sectionid']);
185
186
                foreach ($quizData['questions'] as $question) {
187
                    $categoryId = (int) $question['questioncategoryid'];
188
                    if (!\in_array($categoryId, $questionCategories, true)) {
189
                        $questionCategories[] = $categoryId;
190
                    }
191
                }
192
            }
193
        }
194
195
        if (!empty($questionCategories)) {
196
            $xmlContent .= '  <question_categoryref>'.PHP_EOL;
197
            foreach ($questionCategories as $categoryId) {
198
                $xmlContent .= '    <question_category>'.PHP_EOL;
199
                $xmlContent .= '      <id>'.$categoryId.'</id>'.PHP_EOL;
200
                $xmlContent .= '    </question_category>'.PHP_EOL;
201
            }
202
            $xmlContent .= '  </question_categoryref>'.PHP_EOL;
203
        }
204
205
        // Add a minimal role reference (student)
206
        $xmlContent .= '  <roleref>'.PHP_EOL;
207
        $xmlContent .= '    <role>'.PHP_EOL;
208
        $xmlContent .= '      <id>5</id>'.PHP_EOL;
209
        $xmlContent .= '    </role>'.PHP_EOL;
210
        $xmlContent .= '  </roleref>'.PHP_EOL;
211
212
        $xmlContent .= '</inforef>'.PHP_EOL;
213
214
        file_put_contents($destinationDir.'/inforef.xml', $xmlContent);
215
    }
216
217
    /**
218
     * Creates the roles.xml file.
219
     *
220
     * @param array<int,array<string,mixed>> $rolesData
221
     */
222
    private function createRolesXml(array $rolesData, string $destinationDir): void
223
    {
224
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
225
        $xmlContent .= '<roles>'.PHP_EOL;
226
        foreach ($rolesData as $role) {
227
            $roleName = (string) ($role['name'] ?? 'Student');
228
            $xmlContent .= '  <role>'.PHP_EOL;
229
            $xmlContent .= '    <name>'.htmlspecialchars($roleName).'</name>'.PHP_EOL;
230
            $xmlContent .= '  </role>'.PHP_EOL;
231
        }
232
        $xmlContent .= '</roles>';
233
234
        file_put_contents($destinationDir.'/roles.xml', $xmlContent);
235
    }
236
237
    /**
238
     * Always writes course/calendar.xml (Moodle expects it).
239
     * Priority:
240
     *  1) Events pushed by the builder into $this->course->resources (truth source).
241
     *  2) Fallback to $calendarData (legacy optional).
242
     *  3) Minimal stub if none.
243
     *
244
     * @param array<int,array<string,mixed>> $calendarData
245
     */
246
    private function createCalendarXml(array $calendarData, string $destinationDir): void
247
    {
248
        $builderEvents = $this->collectCalendarEvents();
249
250
        if (!empty($builderEvents)) {
251
            $xml = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
252
            $xml .= '<calendar>' . PHP_EOL;
253
254
            foreach ($builderEvents as $ev) {
255
                // Builder fields: iid, title, content, startDate, endDate, firstPath, firstName, firstSize, firstComment, allDay
256
                $title   = (string) ($ev->title ?? 'Event');
257
                $content = (string) ($ev->content ?? '');
258
                $start   = $this->toTimestamp((string)($ev->startDate ?? ''), time());
259
                $end     = $this->toTimestamp((string)($ev->endDate ?? ''), 0);
260
                $allday  = (int) ($ev->allDay ?? 0);
261
262
                $duration = 0;
263
                if ($end > 0 && $end > $start) {
264
                    $duration = $end - $start;
265
                }
266
267
                // Keep a Moodle-restore-friendly minimal shape
268
                $xml .= "  <event>" . PHP_EOL;
269
                $xml .= '    <name>'.htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8').'</name>' . PHP_EOL;
270
                $xml .= '    <description><![CDATA['.$content.']]></description>' . PHP_EOL;
271
                $xml .= '    <format>1</format>' . PHP_EOL;          // HTML
272
                $xml .= '    <eventtype>course</eventtype>' . PHP_EOL;
273
                $xml .= '    <timestart>'.$start.'</timestart>' . PHP_EOL;
274
                $xml .= '    <duration>'.$duration.'</duration>' . PHP_EOL;
275
                $xml .= '    <visible>1</visible>' . PHP_EOL;
276
                $xml .= '    <allday>'.$allday.'</allday>' . PHP_EOL;
277
                $xml .= '    <repeatid>0</repeatid>' . PHP_EOL;
278
                $xml .= '    <uuid>$@NULL@$</uuid>' . PHP_EOL;
279
                $xml .= "  </event>" . PHP_EOL;
280
            }
281
282
            $xml .= '</calendar>' . PHP_EOL;
283
            file_put_contents($destinationDir . '/calendar.xml', $xml);
284
            return;
285
        }
286
287
        if (!empty($calendarData)) {
288
            $xml  = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
289
            $xml .= '<calendar>' . PHP_EOL;
290
291
            foreach ($calendarData as $e) {
292
                $name      = htmlspecialchars((string)($e['name'] ?? 'Event'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
293
                $timestart = (int)($e['timestart'] ?? time());
294
                $duration  = (int)($e['duration']  ?? 0);
295
296
                $xml .= "  <event>" . PHP_EOL;
297
                $xml .= "    <name>{$name}</name>" . PHP_EOL;
298
                $xml .= "    <timestart>{$timestart}</timestart>" . PHP_EOL;
299
                $xml .= "    <duration>{$duration}</duration>" . PHP_EOL;
300
                $xml .= "  </event>" . PHP_EOL;
301
            }
302
303
            $xml .= '</calendar>' . PHP_EOL;
304
            file_put_contents($destinationDir . '/calendar.xml', $xml);
305
            return;
306
        }
307
308
        $xml  = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
309
        $xml .= '<calendar/>' . PHP_EOL;
310
        file_put_contents($destinationDir . '/calendar.xml', $xml);
311
    }
312
313
    /**
314
     * Creates the comments.xml file.
315
     *
316
     * @param array<int,array<string,mixed>> $commentsData
317
     */
318
    private function createCommentsXml(array $commentsData, string $destinationDir): void
319
    {
320
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
321
        $xmlContent .= '<comments>'.PHP_EOL;
322
        foreach ($commentsData as $comment) {
323
            $content = (string) ($comment['content'] ?? 'No comment');
324
            $author = (string) ($comment['author'] ?? 'Anonymous');
325
326
            $xmlContent .= '  <comment>'.PHP_EOL;
327
            $xmlContent .= '    <content>'.htmlspecialchars($content).'</content>'.PHP_EOL;
328
            $xmlContent .= '    <author>'.htmlspecialchars($author).'</author>'.PHP_EOL;
329
            $xmlContent .= '  </comment>'.PHP_EOL;
330
        }
331
        $xmlContent .= '</comments>';
332
333
        file_put_contents($destinationDir.'/comments.xml', $xmlContent);
334
    }
335
336
    /**
337
     * Creates the competencies.xml file.
338
     *
339
     * @param array<int,array<string,mixed>> $competenciesData
340
     */
341
    private function createCompetenciesXml(array $competenciesData, string $destinationDir): void
342
    {
343
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
344
        $xmlContent .= '<competencies>'.PHP_EOL;
345
        foreach ($competenciesData as $competency) {
346
            $name = (string) ($competency['name'] ?? 'Competency');
347
            $xmlContent .= '  <competency>'.PHP_EOL;
348
            $xmlContent .= '    <name>'.htmlspecialchars($name).'</name>'.PHP_EOL;
349
            $xmlContent .= '  </competency>'.PHP_EOL;
350
        }
351
        $xmlContent .= '</competencies>';
352
353
        file_put_contents($destinationDir.'/competencies.xml', $xmlContent);
354
    }
355
356
    /**
357
     * Creates the completiondefaults.xml file.
358
     *
359
     * @param array<int,array<string,mixed>> $completionData
360
     */
361
    private function createCompletionDefaultsXml(array $completionData, string $destinationDir): void
362
    {
363
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
364
        $xmlContent .= '<completiondefaults>'.PHP_EOL;
365
        foreach ($completionData as $completion) {
366
            $completionState = (int) ($completion['state'] ?? 0);
367
            $xmlContent .= '  <completion>'.PHP_EOL;
368
            $xmlContent .= '    <completionstate>'.$completionState.'</completionstate>'.PHP_EOL;
369
            $xmlContent .= '  </completion>'.PHP_EOL;
370
        }
371
        $xmlContent .= '</completiondefaults>';
372
373
        file_put_contents($destinationDir.'/completiondefaults.xml', $xmlContent);
374
    }
375
376
    /**
377
     * Creates the contentbank.xml file.
378
     *
379
     * @param array<int,array<string,mixed>> $contentBankData
380
     */
381
    private function createContentBankXml(array $contentBankData, string $destinationDir): void
382
    {
383
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
384
        $xmlContent .= '<contentbank>'.PHP_EOL;
385
        foreach ($contentBankData as $content) {
386
            $id = (int) ($content['id'] ?? 0);
387
            $name = (string) ($content['name'] ?? 'Content');
388
            $xmlContent .= '  <content id="'.$id.'">'.htmlspecialchars($name).'</content>'.PHP_EOL;
389
        }
390
        $xmlContent .= '</contentbank>';
391
392
        file_put_contents($destinationDir.'/contentbank.xml', $xmlContent);
393
    }
394
395
    /**
396
     * Creates the filters.xml file.
397
     *
398
     * @param array<int,array<string,mixed>> $filtersData
399
     */
400
    private function createFiltersXml(array $filtersData, string $destinationDir): void
401
    {
402
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
403
        $xmlContent .= '<filters>'.PHP_EOL;
404
        foreach ($filtersData as $filter) {
405
            $filterName = (string) ($filter['name'] ?? 'filter_example');
406
            $active = (int) ($filter['active'] ?? 1);
407
408
            $xmlContent .= '  <filter>'.PHP_EOL;
409
            $xmlContent .= '    <filtername>'.htmlspecialchars($filterName).'</filtername>'.PHP_EOL;
410
            $xmlContent .= '    <active>'.$active.'</active>'.PHP_EOL;
411
            $xmlContent .= '  </filter>'.PHP_EOL;
412
        }
413
        $xmlContent .= '</filters>';
414
415
        file_put_contents($destinationDir.'/filters.xml', $xmlContent);
416
    }
417
418
    /**
419
     * Gather CalendarEvent objects pushed by the builder into $this->course->resources.
420
     * We DO NOT invent types; we rely on the builder's CalendarEvent class in the same namespace.
421
     *
422
     * @return CalendarEvent[]  // from Builder namespace
423
     */
424
    private function collectCalendarEvents(): array
425
    {
426
        $out = [];
427
        $resources = $this->course->resources ?? null;
428
429
        if (!\is_array($resources)) {
430
            return $out;
431
        }
432
433
        // Prefer a dedicated 'calendar' bucket if present; otherwise, scan all buckets.
434
        if (isset($resources['calendar']) && \is_array($resources['calendar'])) {
435
            foreach ($resources['calendar'] as $item) {
436
                if ($item instanceof CalendarEvent) {
437
                    $out[] = $item;
438
                }
439
            }
440
            return $out;
441
        }
442
443
        foreach ($resources as $bucket) {
444
            if (\is_array($bucket)) {
445
                foreach ($bucket as $item) {
446
                    if ($item instanceof CalendarEvent) {
447
                        $out[] = $item;
448
                    }
449
                }
450
            } elseif ($bucket instanceof CalendarEvent) {
451
                $out[] = $bucket;
452
            }
453
        }
454
455
        return $out;
456
    }
457
458
    /**
459
     * Convert a date-string ('Y-m-d H:i:s') or numeric to timestamp with fallback.
460
     */
461
    private function toTimestamp(string $value, int $fallback): int
462
    {
463
        if ($value === '') { return $fallback; }
464
        if (\is_numeric($value)) { return (int) $value; }
465
        $t = \strtotime($value);
466
        return false !== $t ? (int) $t : $fallback;
467
    }
468
}
469