Passed
Pull Request — 1.11.x (#5783)
by Yannick
08:32
created

MoodleExport::exportRootXmlFiles()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 17
c 1
b 0
f 0
dl 0
loc 24
rs 9.7
cc 3
nc 3
nop 1
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
namespace moodleexport;
6
7
use Exception;
8
use FillBlanks;
9
use ZipArchive;
10
use RecursiveIteratorIterator;
11
use RecursiveDirectoryIterator;
12
13
/**
14
 * Class MoodleExport.
15
 * Handles the export of a Moodle course in .mbz format.
16
 *
17
 * @package moodleexport
18
 */
19
class MoodleExport
20
{
21
    private $course;
22
    private static $adminUserData = [];
23
24
    /**
25
     * Constructor to initialize the course object.
26
     */
27
    public function __construct(object $course)
28
    {
29
        $this->course = $course;
30
    }
31
32
    /**
33
     * Export the Moodle course in .mbz format.
34
     */
35
    public function export(string $courseId, string $exportDir, int $version)
36
    {
37
        $tempDir = api_get_path(SYS_ARCHIVE_PATH) . $exportDir;
38
39
        if (!is_dir($tempDir)) {
40
            if (!mkdir($tempDir, api_get_permissions_for_new_directories(), true)) {
41
                throw new Exception(get_lang('ErrorCreatingDirectory'));
42
            }
43
        }
44
45
        $courseInfo = api_get_course_info($courseId);
46
        if (!$courseInfo) {
47
            throw new Exception(get_lang('CourseNotFound'));
48
        }
49
50
        // Generate the moodle_backup.xml
51
        $this->createMoodleBackupXml($tempDir, $version);
52
53
        // Get the activities from the course
54
        $activities = $this->getActivities();
55
56
        // Export course-related files
57
        $courseExport = new CourseExport($this->course, $activities);
58
        $courseExport->exportCourse($tempDir);
59
60
        // Export files-related data and actual files
61
        $fileExport = new FileExport($this->course);
62
        $filesData = $fileExport->getFilesData();
63
        $fileExport->exportFiles($filesData, $tempDir);
64
65
        // Export sections of the course
66
        $this->exportSections($tempDir);
67
68
        // Export all root XML files
69
        $this->exportRootXmlFiles($tempDir);
70
71
        // Compress everything into a .mbz (ZIP) file
72
        $exportedFile = $this->createMbzFile($tempDir);
73
74
        // Clean up temporary directory
75
        $this->cleanupTempDir($tempDir);
76
77
        return $exportedFile;
78
    }
79
80
    /**
81
     * Export root XML files such as badges, completion, gradebook, etc.
82
     */
83
    private function exportRootXmlFiles(string $exportDir): void
84
    {
85
        $this->exportBadgesXml($exportDir);
86
        $this->exportCompletionXml($exportDir);
87
        $this->exportGradebookXml($exportDir);
88
        $this->exportGradeHistoryXml($exportDir);
89
        $this->exportGroupsXml($exportDir);
90
        $this->exportOutcomesXml($exportDir);
91
92
        // Export quizzes and their questions
93
        $activities = $this->getActivities();
94
        $questionsData = [];
95
        foreach ($activities as $activity) {
96
            if ($activity['modulename'] === 'quiz') {
97
                $quizExport = new QuizExport($this->course);
98
                $quizData = $quizExport->getData($activity['id'], $activity['sectionid']);
99
                $questionsData[] = $quizData;
100
            }
101
        }
102
        $this->exportQuestionsXml($questionsData, $exportDir);
103
104
        $this->exportRolesXml($exportDir);
105
        $this->exportScalesXml($exportDir);
106
        $this->exportUsersXml($exportDir);
107
    }
108
109
    /**
110
     * Create the moodle_backup.xml file with the required course details.
111
     */
112
    private function createMoodleBackupXml(string $destinationDir, int $version): void
113
    {
114
        // Generate course information and backup metadata
115
        $courseInfo = api_get_course_info($this->course->code);
116
        $backupId = md5(uniqid(mt_rand(), true));
117
        $siteHash = md5(uniqid(mt_rand(), true));
118
        $wwwRoot = api_get_path(WEB_PATH);
119
120
        // Build the XML content for the backup
121
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
122
        $xmlContent .= '<moodle_backup>' . PHP_EOL;
123
        $xmlContent .= '  <information>' . PHP_EOL;
124
125
        $xmlContent .= '    <name>backup-' . htmlspecialchars($courseInfo['code']) . '.mbz</name>' . PHP_EOL;
126
        $xmlContent .= '    <moodle_version>' . ($version === 3 ? '2021051718' : '2022041900') . '</moodle_version>' . PHP_EOL;
127
        $xmlContent .= '    <moodle_release>' . ($version === 3 ? '3.11.18 (Build: 20231211)' : '4.x version here') . '</moodle_release>' . PHP_EOL;
128
        $xmlContent .= '    <backup_version>' . ($version === 3 ? '2021051700' : '2022041900') . '</backup_version>' . PHP_EOL;
129
        $xmlContent .= '    <backup_release>' . ($version === 3 ? '3.11' : '4.x') . '</backup_release>' . PHP_EOL;
130
        $xmlContent .= '    <backup_date>' . time() . '</backup_date>' . PHP_EOL;
131
        $xmlContent .= '    <mnet_remoteusers>0</mnet_remoteusers>' . PHP_EOL;
132
        $xmlContent .= '    <include_files>1</include_files>' . PHP_EOL;
133
        $xmlContent .= '    <include_file_references_to_external_content>0</include_file_references_to_external_content>' . PHP_EOL;
134
        $xmlContent .= '    <original_wwwroot>' . $wwwRoot . '</original_wwwroot>' . PHP_EOL;
135
        $xmlContent .= '    <original_site_identifier_hash>' . $siteHash . '</original_site_identifier_hash>' . PHP_EOL;
136
        $xmlContent .= '    <original_course_id>' . htmlspecialchars($courseInfo['real_id']) . '</original_course_id>' . PHP_EOL;
137
        $xmlContent .= '    <original_course_format>' . get_lang('Topics') . '</original_course_format>' . PHP_EOL;
138
        $xmlContent .= '    <original_course_fullname>' . htmlspecialchars($courseInfo['title']) . '</original_course_fullname>' . PHP_EOL;
139
        $xmlContent .= '    <original_course_shortname>' . htmlspecialchars($courseInfo['code']) . '</original_course_shortname>' . PHP_EOL;
140
        $xmlContent .= '    <original_course_startdate>' . $courseInfo['startdate'] . '</original_course_startdate>' . PHP_EOL;
141
        $xmlContent .= '    <original_course_enddate>' . $courseInfo['enddate'] . '</original_course_enddate>' . PHP_EOL;
142
        $xmlContent .= '    <original_course_contextid>' . $courseInfo['real_id'] . '</original_course_contextid>' . PHP_EOL;
143
        $xmlContent .= '    <original_system_contextid>' . api_get_current_access_url_id() . '</original_system_contextid>' . PHP_EOL;
144
145
        $xmlContent .= '    <details>' . PHP_EOL;
146
        $xmlContent .= '      <detail backup_id="' . $backupId . '">' . PHP_EOL;
147
        $xmlContent .= '        <type>course</type>' . PHP_EOL;
148
        $xmlContent .= '        <format>moodle2</format>' . PHP_EOL;
149
        $xmlContent .= '        <interactive>1</interactive>' . PHP_EOL;
150
        $xmlContent .= '        <mode>10</mode>' . PHP_EOL;
151
        $xmlContent .= '        <execution>1</execution>' . PHP_EOL;
152
        $xmlContent .= '        <executiontime>0</executiontime>' . PHP_EOL;
153
        $xmlContent .= '      </detail>' . PHP_EOL;
154
        $xmlContent .= '    </details>' . PHP_EOL;
155
156
        // Contents with activities and sections
157
        $xmlContent .= '    <contents>' . PHP_EOL;
158
159
        // Export sections dynamically and add them to the XML
160
        $sections = $this->getSections();
161
        if (!empty($sections)) {
162
            $xmlContent .= '      <sections>' . PHP_EOL;
163
            foreach ($sections as $section) {
164
                $xmlContent .= '        <section>' . PHP_EOL;
165
                $xmlContent .= '          <sectionid>' . $section['id'] . '</sectionid>' . PHP_EOL;
166
                $xmlContent .= '          <title>' . htmlspecialchars($section['name']) . '</title>' . PHP_EOL;
167
                $xmlContent .= '          <directory>sections/section_' . $section['id'] . '</directory>' . PHP_EOL;
168
                $xmlContent .= '        </section>' . PHP_EOL;
169
            }
170
            $xmlContent .= '      </sections>' . PHP_EOL;
171
        }
172
173
        $activities = $this->getActivities();
174
        if (!empty($activities)) {
175
            $xmlContent .= '      <activities>' . PHP_EOL;
176
            foreach ($activities as $activity) {
177
                $xmlContent .= '        <activity>' . PHP_EOL;
178
                $xmlContent .= '          <moduleid>' . $activity['moduleid'] . '</moduleid>' . PHP_EOL;
179
                $xmlContent .= '          <sectionid>' . $activity['sectionid'] . '</sectionid>' . PHP_EOL;
180
                $xmlContent .= '          <modulename>' . htmlspecialchars($activity['modulename']) . '</modulename>' . PHP_EOL;
181
                $xmlContent .= '          <title>' . htmlspecialchars($activity['title']) . '</title>' . PHP_EOL;
182
                $xmlContent .= '          <directory>activities/' . $activity['modulename'] . '_' . $activity['moduleid'] . '</directory>' . PHP_EOL;
183
                $xmlContent .= '        </activity>' . PHP_EOL;
184
            }
185
            $xmlContent .= '      </activities>' . PHP_EOL;
186
        }
187
188
        // Course directory
189
        $xmlContent .= '      <course>' . PHP_EOL;
190
        $xmlContent .= '        <courseid>' . $courseInfo['real_id'] . '</courseid>' . PHP_EOL;
191
        $xmlContent .= '        <title>' . htmlspecialchars($courseInfo['title']) . '</title>' . PHP_EOL;
192
        $xmlContent .= '        <directory>course</directory>' . PHP_EOL;
193
        $xmlContent .= '      </course>' . PHP_EOL;
194
195
        $xmlContent .= '    </contents>' . PHP_EOL;
196
197
        // Backup settings
198
        $xmlContent .= '    <settings>' . PHP_EOL;
199
        $settings = $this->exportBackupSettings($sections, $activities);
200
        foreach ($settings as $setting) {
201
            $xmlContent .= '      <setting>' . PHP_EOL;
202
            $xmlContent .= '        <level>' . htmlspecialchars($setting['level']) . '</level>' . PHP_EOL;
203
            $xmlContent .= '        <name>' . htmlspecialchars($setting['name']) . '</name>' . PHP_EOL;
204
            $xmlContent .= '        <value>' . $setting['value'] . '</value>' . PHP_EOL;
205
            if (isset($setting['section'])) {
206
                $xmlContent .= '        <section>' . htmlspecialchars($setting['section']) . '</section>' . PHP_EOL;
207
            }
208
            if (isset($setting['activity'])) {
209
                $xmlContent .= '        <activity>' . htmlspecialchars($setting['activity']) . '</activity>' . PHP_EOL;
210
            }
211
            $xmlContent .= '      </setting>' . PHP_EOL;
212
        }
213
        $xmlContent .= '    </settings>' . PHP_EOL;
214
215
        $xmlContent .= '  </information>' . PHP_EOL;
216
        $xmlContent .= '</moodle_backup>';
217
218
        $xmlFile = $destinationDir . '/moodle_backup.xml';
219
        file_put_contents($xmlFile, $xmlContent);
220
    }
221
222
    /**
223
     * Get all sections from the course.
224
     */
225
    private function getSections(): array
226
    {
227
        $sectionExport = new SectionExport($this->course);
228
        $sections = [];
229
230
        foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) {
231
            if ($learnpath->lp_type == '1') {
232
                $sections[] = $sectionExport->getSectionData($learnpath);
233
            }
234
        }
235
236
        // Add a general section for resources without a lesson
237
        $sections[] = [
238
            'id' => 0,
239
            'number' => 0,
240
            'name' => get_lang('General'),
241
            'summary' => get_lang('GeneralResourcesCourse'),
242
            'sequence' => 0,
243
            'visible' => 1,
244
            'timemodified' => time(),
245
            'activities' => $sectionExport->getActivitiesForGeneral(),
246
        ];
247
248
        return $sections;
249
    }
250
251
    /**
252
     * Get all activities from the course.
253
     */
254
    private function getActivities(): array
255
    {
256
        $activities = [];
257
        $glossaryAdded = false;
258
259
        foreach ($this->course->resources as $resourceType => $resources) {
260
            foreach ($resources as $resource) {
261
                $exportClass = null;
262
                $moduleName = '';
263
                $title = '';
264
                $id = 0;
265
266
                // Handle quizzes
267
                if ($resourceType === RESOURCE_QUIZ && $resource->obj->iid > 0) {
268
                    $exportClass = QuizExport::class;
269
                    $moduleName = 'quiz';
270
                    $id = $resource->obj->iid;
271
                    $title = $resource->obj->title;
272
                }
273
                // Handle links
274
                if ($resourceType === RESOURCE_LINK && $resource->source_id > 0) {
275
                    $exportClass = UrlExport::class;
276
                    $moduleName = 'url';
277
                    $id = $resource->source_id;
278
                    $title = $resource->title;
279
                }
280
                // Handle glossaries
281
                elseif ($resourceType === RESOURCE_GLOSSARY && $resource->glossary_id > 0 && !$glossaryAdded) {
282
                    $exportClass = GlossaryExport::class;
283
                    $moduleName = 'glossary';
284
                    $id = 1;
285
                    $title = get_lang('Glossary');
286
                    $glossaryAdded = true;
287
                }
288
                // Handle forums
289
                elseif ($resourceType === RESOURCE_FORUM && $resource->source_id > 0) {
290
                    $exportClass = ForumExport::class;
291
                    $moduleName = 'forum';
292
                    $id = $resource->obj->iid;
293
                    $title = $resource->obj->forum_title;
294
                }
295
                // Handle documents (HTML pages)
296
                elseif ($resourceType === RESOURCE_DOCUMENT && $resource->source_id > 0) {
297
                    $document = \DocumentManager::get_document_data_by_id($resource->source_id, $this->course->code);
298
                    if ('html' === pathinfo($document['path'], PATHINFO_EXTENSION)) {
299
                        $exportClass = PageExport::class;
300
                        $moduleName = 'page';
301
                        $id = $resource->source_id;
302
                        $title = $document['title'];
303
                    } elseif ('file' === $resource->file_type) {
304
                        $exportClass = ResourceExport::class;
305
                        $moduleName = 'resource';
306
                        $id = $resource->source_id;
307
                        $title = $resource->title;
308
                    } elseif ('folder' === $resource->file_type) {
309
                        $exportClass = FolderExport::class;
310
                        $moduleName = 'folder';
311
                        $id = $resource->source_id;
312
                        $title = $resource->title;
313
                    }
314
                }
315
                // Handle assignments (work)
316
                elseif ($resourceType === RESOURCE_WORK && $resource->source_id > 0) {
317
                    $exportClass = AssignExport::class;
318
                    $moduleName = 'assign';
319
                    $id = $resource->source_id;
320
                    $title = $resource->params['title'] ?? '';
321
                }
322
                // Handle feedback (survey)
323
                elseif ($resourceType === RESOURCE_SURVEY && $resource->source_id > 0) {
324
                    $exportClass = FeedbackExport::class;
325
                    $moduleName = 'feedback';
326
                    $id = $resource->source_id;
327
                    $title = $resource->params['title'] ?? '';
328
                }
329
330
                // Add the activity if the class and module name are set
331
                if ($exportClass && $moduleName) {
332
                    $exportInstance = new $exportClass($this->course);
333
                    $activities[] = [
334
                        'id' => $id,
335
                        'sectionid' => $exportInstance->getSectionIdForActivity($id, $resourceType),
336
                        'modulename' => $moduleName,
337
                        'moduleid' => $id,
338
                        'title' => $title,
339
                    ];
340
                }
341
            }
342
        }
343
344
        return $activities;
345
    }
346
347
    /**
348
     * Export the sections of the course.
349
     */
350
    private function exportSections(string $exportDir): void
351
    {
352
        $sections = $this->getSections();
353
354
        foreach ($sections as $section) {
355
            $sectionExport = new SectionExport($this->course);
356
            $sectionExport->exportSection($section['id'], $exportDir);
357
        }
358
    }
359
360
    /**
361
     * Create a .mbz (ZIP) file from the exported data.
362
     */
363
    private function createMbzFile(string $sourceDir): string
364
    {
365
        $zip = new ZipArchive();
366
        $zipFile = $sourceDir . '.mbz';
367
368
        if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
369
            throw new Exception(get_lang('ErrorCreatingZip'));
370
        }
371
372
        $files = new RecursiveIteratorIterator(
373
            new RecursiveDirectoryIterator($sourceDir),
374
            RecursiveIteratorIterator::LEAVES_ONLY
375
        );
376
377
        foreach ($files as $file) {
378
            if (!$file->isDir()) {
379
                $filePath = $file->getRealPath();
380
                $relativePath = substr($filePath, strlen($sourceDir) + 1);
381
382
                if (!$zip->addFile($filePath, $relativePath)) {
383
                    throw new Exception(get_lang('ErrorAddingFileToZip') . ": $relativePath");
384
                }
385
            }
386
        }
387
388
        if (!$zip->close()) {
389
            throw new Exception(get_lang('ErrorClosingZip'));
390
        }
391
392
        return $zipFile;
393
    }
394
395
    /**
396
     * Clean up the temporary directory used for export.
397
     */
398
    private function cleanupTempDir(string $dir): void
399
    {
400
        $this->recursiveDelete($dir);
401
    }
402
403
    /**
404
     * Recursively delete a directory and its contents.
405
     */
406
    private function recursiveDelete(string $dir): void
407
    {
408
        $files = array_diff(scandir($dir), ['.', '..']);
409
        foreach ($files as $file) {
410
            $path = "$dir/$file";
411
            is_dir($path) ? $this->recursiveDelete($path) : unlink($path);
412
        }
413
        rmdir($dir);
414
    }
415
416
    /**
417
     * Export badges data to XML file.
418
     */
419
    private function exportBadgesXml(string $exportDir): void
420
    {
421
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
422
        $xmlContent .= '<badges>' . PHP_EOL;
423
        $xmlContent .= '</badges>';
424
        file_put_contents($exportDir . '/badges.xml', $xmlContent);
425
    }
426
427
    /**
428
     * Export course completion data to XML file.
429
     */
430
    private function exportCompletionXml(string $exportDir): void
431
    {
432
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
433
        $xmlContent .= '<completions>' . PHP_EOL;
434
        $xmlContent .= '</completions>';
435
        file_put_contents($exportDir . '/completion.xml', $xmlContent);
436
    }
437
438
    /**
439
     * Export gradebook data to XML file.
440
     */
441
    private function exportGradebookXml(string $exportDir): void
442
    {
443
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
444
        $xmlContent .= '<gradebook>' . PHP_EOL;
445
        $xmlContent .= '</gradebook>';
446
        file_put_contents($exportDir . '/gradebook.xml', $xmlContent);
447
    }
448
449
    /**
450
     * Export grade history data to XML file.
451
     */
452
    private function exportGradeHistoryXml(string $exportDir): void
453
    {
454
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
455
        $xmlContent .= '<grade_history>' . PHP_EOL;
456
        $xmlContent .= '</grade_history>';
457
        file_put_contents($exportDir . '/grade_history.xml', $xmlContent);
458
    }
459
460
    /**
461
     * Export groups data to XML file.
462
     */
463
    private function exportGroupsXml(string $exportDir): void
464
    {
465
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
466
        $xmlContent .= '<groups>' . PHP_EOL;
467
        $xmlContent .= '</groups>';
468
        file_put_contents($exportDir . '/groups.xml', $xmlContent);
469
    }
470
471
    /**
472
     * Export outcomes data to XML file.
473
     */
474
    private function exportOutcomesXml(string $exportDir): void
475
    {
476
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
477
        $xmlContent .= '<outcomes>' . PHP_EOL;
478
        $xmlContent .= '</outcomes>';
479
        file_put_contents($exportDir . '/outcomes.xml', $xmlContent);
480
    }
481
482
    /**
483
     * Export questions data to XML file.
484
     */
485
    public function exportQuestionsXml(array $questionsData, string $exportDir): void
486
    {
487
        $quizExport = new QuizExport($this->course);
488
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
489
        $xmlContent .= '<question_categories>' . PHP_EOL;
490
491
        foreach ($questionsData as $quiz) {
492
            $categoryId = $quiz['questions'][0]['questioncategoryid'] ?? '0';
493
494
            $xmlContent .= '  <question_category id="' . $categoryId . '">' . PHP_EOL;
495
            $xmlContent .= '    <name>Default for ' . htmlspecialchars($quiz['name'] ?? 'Unknown') . '</name>' . PHP_EOL;
496
            $xmlContent .= '    <contextid>' . ($quiz['contextid'] ?? '0') . '</contextid>' . PHP_EOL;
497
            $xmlContent .= '    <contextlevel>70</contextlevel>' . PHP_EOL;
498
            $xmlContent .= '    <contextinstanceid>' . ($quiz['moduleid'] ?? '0') . '</contextinstanceid>' . PHP_EOL;
499
            $xmlContent .= '    <info>The default category for questions shared in context "' . htmlspecialchars($quiz['name'] ?? 'Unknown') . '".</info>' . PHP_EOL;
500
            $xmlContent .= '    <infoformat>0</infoformat>' . PHP_EOL;
501
            $xmlContent .= '    <stamp>moodle+' . time() . '+CATEGORYSTAMP</stamp>' . PHP_EOL;
502
            $xmlContent .= '    <parent>0</parent>' . PHP_EOL;
503
            $xmlContent .= '    <sortorder>999</sortorder>' . PHP_EOL;
504
            $xmlContent .= '    <idnumber>$@NULL@$</idnumber>' . PHP_EOL;
505
            $xmlContent .= '    <questions>' . PHP_EOL;
506
507
            foreach ($quiz['questions'] as $question) {
508
                $xmlContent .= $quizExport->exportQuestion($question);
509
            }
510
511
            $xmlContent .= '    </questions>' . PHP_EOL;
512
            $xmlContent .= '  </question_category>' . PHP_EOL;
513
        }
514
515
        $xmlContent .= '</question_categories>';
516
        file_put_contents($exportDir . '/questions.xml', $xmlContent);
517
    }
518
519
    /**
520
     * Export roles data to XML file.
521
     */
522
    private function exportRolesXml(string $exportDir): void
523
    {
524
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
525
        $xmlContent .= '<roles_definition>' . PHP_EOL;
526
        $xmlContent .= '  <role id="5">' . PHP_EOL;
527
        $xmlContent .= '    <name></name>' . PHP_EOL;
528
        $xmlContent .= '    <shortname>student</shortname>' . PHP_EOL;
529
        $xmlContent .= '    <nameincourse>$@NULL@$</nameincourse>' . PHP_EOL;
530
        $xmlContent .= '    <description></description>' . PHP_EOL;
531
        $xmlContent .= '    <sortorder>5</sortorder>' . PHP_EOL;
532
        $xmlContent .= '    <archetype>student</archetype>' . PHP_EOL;
533
        $xmlContent .= '  </role>' . PHP_EOL;
534
        $xmlContent .= '</roles_definition>' . PHP_EOL;
535
536
        file_put_contents($exportDir . '/roles.xml', $xmlContent);
537
    }
538
539
    /**
540
     * Export scales data to XML file.
541
     */
542
    private function exportScalesXml(string $exportDir): void
543
    {
544
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
545
        $xmlContent .= '<scales>' . PHP_EOL;
546
        $xmlContent .= '</scales>';
547
        file_put_contents($exportDir . '/scales.xml', $xmlContent);
548
    }
549
550
    /**
551
     * Sets the admin user data.
552
     */
553
    public function setAdminUserData(int $id, string $username, string $email): void
554
    {
555
        self::$adminUserData = [
556
            'id' => $id,
557
            'contextid' => $id,
558
            'username' => $username,
559
            'idnumber' => '',
560
            'email' => $email,
561
            'phone1' => '',
562
            'phone2' => '',
563
            'institution' => '',
564
            'department' => '',
565
            'address' => '',
566
            'city' => 'London',
567
            'country' => 'GB',
568
            'lastip' => '127.0.0.1',
569
            'picture' => '0',
570
            'description' => '',
571
            'descriptionformat' => 1,
572
            'imagealt' => '$@NULL@$',
573
            'auth' => 'manual',
574
            'firstname' => 'Admin',
575
            'lastname' => 'User',
576
            'confirmed' => 1,
577
            'policyagreed' => 0,
578
            'deleted' => 0,
579
            'lang' => 'en',
580
            'theme' => '',
581
            'timezone' => 99,
582
            'firstaccess' => time(),
583
            'lastaccess' => time() - (60 * 60 * 24 * 7),
584
            'lastlogin' => time() - (60 * 60 * 24 * 2),
585
            'currentlogin' => time(),
586
            'mailformat' => 1,
587
            'maildigest' => 0,
588
            'maildisplay' => 1,
589
            'autosubscribe' => 1,
590
            'trackforums' => 0,
591
            'timecreated' => time(),
592
            'timemodified' => time(),
593
            'trustbitmask' => 0,
594
            'preferences' => [
595
                ['name' => 'core_message_migrate_data', 'value' => 1],
596
                ['name' => 'auth_manual_passwordupdatetime', 'value' => time()],
597
                ['name' => 'email_bounce_count', 'value' => 1],
598
                ['name' => 'email_send_count', 'value' => 1],
599
                ['name' => 'login_failed_count_since_success', 'value' => 0],
600
                ['name' => 'filepicker_recentrepository', 'value' => 5],
601
                ['name' => 'filepicker_recentlicense', 'value' => 'unknown'],
602
            ],
603
        ];
604
    }
605
606
    /**
607
     * Returns hardcoded data for the admin user.
608
     *
609
     * @return array
610
     */
611
    public static function getAdminUserData(): array
612
    {
613
        return self::$adminUserData;
614
    }
615
616
    /**
617
     * Export the user XML with admin user data.
618
     */
619
    private function exportUsersXml(string $exportDir): void
620
    {
621
        $adminData = self::getAdminUserData();
622
623
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
624
        $xmlContent .= '<users>' . PHP_EOL;
625
        $xmlContent .= '  <user id="' . $adminData['id'] . '" contextid="' . $adminData['contextid'] . '">' . PHP_EOL;
626
        $xmlContent .= '    <username>' . $adminData['username'] . '</username>' . PHP_EOL;
627
        $xmlContent .= '    <idnumber>' . $adminData['idnumber'] . '</idnumber>' . PHP_EOL;
628
        $xmlContent .= '    <email>' . $adminData['email'] . '</email>' . PHP_EOL;
629
        $xmlContent .= '    <phone1>' . $adminData['phone1'] . '</phone1>' . PHP_EOL;
630
        $xmlContent .= '    <phone2>' . $adminData['phone2'] . '</phone2>' . PHP_EOL;
631
        $xmlContent .= '    <institution>' . $adminData['institution'] . '</institution>' . PHP_EOL;
632
        $xmlContent .= '    <department>' . $adminData['department'] . '</department>' . PHP_EOL;
633
        $xmlContent .= '    <address>' . $adminData['address'] . '</address>' . PHP_EOL;
634
        $xmlContent .= '    <city>' . $adminData['city'] . '</city>' . PHP_EOL;
635
        $xmlContent .= '    <country>' . $adminData['country'] . '</country>' . PHP_EOL;
636
        $xmlContent .= '    <lastip>' . $adminData['lastip'] . '</lastip>' . PHP_EOL;
637
        $xmlContent .= '    <picture>' . $adminData['picture'] . '</picture>' . PHP_EOL;
638
        $xmlContent .= '    <description>' . $adminData['description'] . '</description>' . PHP_EOL;
639
        $xmlContent .= '    <descriptionformat>' . $adminData['descriptionformat'] . '</descriptionformat>' . PHP_EOL;
640
        $xmlContent .= '    <imagealt>' . $adminData['imagealt'] . '</imagealt>' . PHP_EOL;
641
        $xmlContent .= '    <auth>' . $adminData['auth'] . '</auth>' . PHP_EOL;
642
        $xmlContent .= '    <firstname>' . $adminData['firstname'] . '</firstname>' . PHP_EOL;
643
        $xmlContent .= '    <lastname>' . $adminData['lastname'] . '</lastname>' . PHP_EOL;
644
        $xmlContent .= '    <confirmed>' . $adminData['confirmed'] . '</confirmed>' . PHP_EOL;
645
        $xmlContent .= '    <policyagreed>' . $adminData['policyagreed'] . '</policyagreed>' . PHP_EOL;
646
        $xmlContent .= '    <deleted>' . $adminData['deleted'] . '</deleted>' . PHP_EOL;
647
        $xmlContent .= '    <lang>' . $adminData['lang'] . '</lang>' . PHP_EOL;
648
        $xmlContent .= '    <theme>' . $adminData['theme'] . '</theme>' . PHP_EOL;
649
        $xmlContent .= '    <timezone>' . $adminData['timezone'] . '</timezone>' . PHP_EOL;
650
        $xmlContent .= '    <firstaccess>' . $adminData['firstaccess'] . '</firstaccess>' . PHP_EOL;
651
        $xmlContent .= '    <lastaccess>' . $adminData['lastaccess'] . '</lastaccess>' . PHP_EOL;
652
        $xmlContent .= '    <lastlogin>' . $adminData['lastlogin'] . '</lastlogin>' . PHP_EOL;
653
        $xmlContent .= '    <currentlogin>' . $adminData['currentlogin'] . '</currentlogin>' . PHP_EOL;
654
        $xmlContent .= '    <mailformat>' . $adminData['mailformat'] . '</mailformat>' . PHP_EOL;
655
        $xmlContent .= '    <maildigest>' . $adminData['maildigest'] . '</maildigest>' . PHP_EOL;
656
        $xmlContent .= '    <maildisplay>' . $adminData['maildisplay'] . '</maildisplay>' . PHP_EOL;
657
        $xmlContent .= '    <autosubscribe>' . $adminData['autosubscribe'] . '</autosubscribe>' . PHP_EOL;
658
        $xmlContent .= '    <trackforums>' . $adminData['trackforums'] . '</trackforums>' . PHP_EOL;
659
        $xmlContent .= '    <timecreated>' . $adminData['timecreated'] . '</timecreated>' . PHP_EOL;
660
        $xmlContent .= '    <timemodified>' . $adminData['timemodified'] . '</timemodified>' . PHP_EOL;
661
        $xmlContent .= '    <trustbitmask>' . $adminData['trustbitmask'] . '</trustbitmask>' . PHP_EOL;
662
663
        // Preferences
664
        if (isset($adminData['preferences']) && is_array($adminData['preferences'])) {
665
            $xmlContent .= '    <preferences>' . PHP_EOL;
666
            foreach ($adminData['preferences'] as $preference) {
667
                $xmlContent .= '      <preference>' . PHP_EOL;
668
                $xmlContent .= '        <name>' . htmlspecialchars($preference['name']) . '</name>' . PHP_EOL;
669
                $xmlContent .= '        <value>' . htmlspecialchars($preference['value']) . '</value>' . PHP_EOL;
670
                $xmlContent .= '      </preference>' . PHP_EOL;
671
            }
672
            $xmlContent .= '    </preferences>' . PHP_EOL;
673
        } else {
674
            $xmlContent .= '    <preferences></preferences>' . PHP_EOL;
675
        }
676
677
        // Roles (empty for now)
678
        $xmlContent .= '    <roles>' . PHP_EOL;
679
        $xmlContent .= '      <role_overrides></role_overrides>' . PHP_EOL;
680
        $xmlContent .= '      <role_assignments></role_assignments>' . PHP_EOL;
681
        $xmlContent .= '    </roles>' . PHP_EOL;
682
683
        $xmlContent .= '  </user>' . PHP_EOL;
684
        $xmlContent .= '</users>';
685
686
        // Save the content to the users.xml file
687
        file_put_contents($exportDir . '/users.xml', $xmlContent);
688
    }
689
690
    /**
691
     * Export the backup settings, including dynamic settings for sections and activities.
692
     */
693
    private function exportBackupSettings(array $sections, array $activities): array
694
    {
695
        // root-level settings
696
        $settings = [
697
            ['level' => 'root', 'name' => 'filename', 'value' => 'backup-moodle-course-' . time() . '.mbz'],
698
            ['level' => 'root', 'name' => 'imscc11', 'value' => '0'],
699
            ['level' => 'root', 'name' => 'users', 'value' => '1'],
700
            ['level' => 'root', 'name' => 'anonymize', 'value' => '0'],
701
            ['level' => 'root', 'name' => 'role_assignments', 'value' => '1'],
702
            ['level' => 'root', 'name' => 'activities', 'value' => '1'],
703
            ['level' => 'root', 'name' => 'blocks', 'value' => '1'],
704
            ['level' => 'root', 'name' => 'files', 'value' => '1'],
705
            ['level' => 'root', 'name' => 'filters', 'value' => '1'],
706
            ['level' => 'root', 'name' => 'comments', 'value' => '1'],
707
            ['level' => 'root', 'name' => 'badges', 'value' => '1'],
708
            ['level' => 'root', 'name' => 'calendarevents', 'value' => '1'],
709
            ['level' => 'root', 'name' => 'userscompletion', 'value' => '1'],
710
            ['level' => 'root', 'name' => 'logs', 'value' => '0'],
711
            ['level' => 'root', 'name' => 'grade_histories', 'value' => '0'],
712
            ['level' => 'root', 'name' => 'questionbank', 'value' => '1'],
713
            ['level' => 'root', 'name' => 'groups', 'value' => '1'],
714
            ['level' => 'root', 'name' => 'competencies', 'value' => '0'],
715
            ['level' => 'root', 'name' => 'customfield', 'value' => '1'],
716
            ['level' => 'root', 'name' => 'contentbankcontent', 'value' => '1'],
717
            ['level' => 'root', 'name' => 'legacyfiles', 'value' => '1'],
718
        ];
719
720
        // section-level settings
721
        foreach ($sections as $section) {
722
            $settings[] = [
723
                'level' => 'section',
724
                'section' => 'section_' . $section['id'],
725
                'name' => 'section_' . $section['id'] . '_included',
726
                'value' => '1',
727
            ];
728
            $settings[] = [
729
                'level' => 'section',
730
                'section' => 'section_' . $section['id'],
731
                'name' => 'section_' . $section['id'] . '_userinfo',
732
                'value' => '1',
733
            ];
734
        }
735
736
        // activity-level settings
737
        foreach ($activities as $activity) {
738
            $settings[] = [
739
                'level' => 'activity',
740
                'activity' => $activity['modulename'] . '_' . $activity['moduleid'],
741
                'name' => $activity['modulename'] . '_' . $activity['moduleid'] . '_included',
742
                'value' => '1',
743
            ];
744
            $settings[] = [
745
                'level' => 'activity',
746
                'activity' => $activity['modulename'] . '_' . $activity['moduleid'],
747
                'name' => $activity['modulename'] . '_' . $activity['moduleid'] . '_userinfo',
748
                'value' => '1',
749
            ];
750
        }
751
752
        return $settings;
753
    }
754
}
755