Completed
Push — 1.11.x ( e34e1a...ec0fb4 )
by
unknown
01:54 queued 51s
created

MoodleExport::fillQuestionsFromQuiz()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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