Passed
Push — master ( 24a37f...f52bbb )
by
unknown
24:30 queued 12:07
created

MoodleExport::exportScalesXml()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 6
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\CourseBuilder;
10
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\ActivityExport;
11
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\AssignExport;
12
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\FeedbackExport;
13
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\ForumExport;
14
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\GlossaryExport;
15
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\LabelExport;
16
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\PageExport;
17
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\QuizExport;
18
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\ResourceExport;
19
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\UrlExport;
20
use Exception;
21
use RecursiveDirectoryIterator;
22
use RecursiveIteratorIterator;
23
use ZipArchive;
24
25
use const PATHINFO_EXTENSION;
26
use const PHP_EOL;
27
28
/**
29
 * Class MoodleExport.
30
 * Handles the export of a Moodle course in .mbz format.
31
 */
32
class MoodleExport
33
{
34
    /**
35
     * @var object
36
     */
37
    private $course;
38
39
    /**
40
     * @var array<string,mixed>
41
     */
42
    private static $adminUserData = [];
43
44
    /**
45
     * @var bool selection flag (true when exporting only selected items)
46
     */
47
    private bool $selectionMode = false;
48
49
    protected static array $activityUserinfo = [];
50
51
    /**
52
     * Constructor to initialize the course object.
53
     *
54
     * @param object $course        Filtered legacy course (may be full or selected-only)
55
     * @param bool   $selectionMode When true, do NOT re-hydrate from complete snapshot
56
     */
57
    public function __construct(object $course, bool $selectionMode = false)
58
    {
59
        // Keep the provided (possibly filtered) course as-is.
60
        $this->course = $course;
61
        $this->selectionMode = $selectionMode;
62
63
        // Only auto-fill missing dependencies when doing a full export.
64
        // In selection mode we must not re-inject extra content ("full backup" effect).
65
        if (!$this->selectionMode) {
66
            $cb = new CourseBuilder('complete');
67
            $complete = $cb->build(0, (string) ($course->code ?? ''));
68
69
            // Fill missing resources from learnpath (full export only)
70
            $this->fillResourcesFromLearnpath($complete);
71
72
            // Fill missing quiz questions (full export only)
73
            $this->fillQuestionsFromQuiz($complete);
74
        }
75
    }
76
77
    /**
78
     * Export the Moodle course in .mbz format.
79
     *
80
     * @return string Path to the created .mbz file
81
     */
82
    public function export(string $courseId, string $exportDir, int $version)
83
    {
84
        @error_log('[MoodleExport::export] Start. courseId='.$courseId.' exportDir='.$exportDir.' version='.$version);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for error_log(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

84
        /** @scrutinizer ignore-unhandled */ @error_log('[MoodleExport::export] Start. courseId='.$courseId.' exportDir='.$exportDir.' version='.$version);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
85
86
        $tempDir = api_get_path(SYS_ARCHIVE_PATH).$exportDir;
87
88
        if (!is_dir($tempDir)) {
89
            if (!mkdir($tempDir, api_get_permissions_for_new_directories(), true)) {
90
                @error_log('[MoodleExport::export] ERROR cannot create tempDir='.$tempDir);
91
                throw new Exception(get_lang('ErrorCreatingDirectory'));
92
            }
93
            @error_log('[MoodleExport::export] Created tempDir='.$tempDir);
94
        }
95
96
        $courseInfo = api_get_course_info($courseId);
97
        if (!$courseInfo) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $courseInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
98
            @error_log('[MoodleExport::export] ERROR CourseNotFound id='.$courseId);
99
            throw new Exception(get_lang('CourseNotFound'));
100
        }
101
102
        // 1) Create Moodle backup skeleton (backup.xml + dirs)
103
        $this->createMoodleBackupXml($tempDir, $version);
104
        @error_log('[MoodleExport::export] moodle_backup.xml generated');
105
106
        //    This must happen BEFORE calling getActivities() so they are included.
107
        if (method_exists($this, 'enqueueUrlActivities')) {
108
            @error_log('[MoodleExport::export] Enqueuing URL activities …');
109
            $this->enqueueUrlActivities();
110
            @error_log('[MoodleExport::export] URL activities enqueued');
111
        } else {
112
            @error_log('[MoodleExport::export][WARN] enqueueUrlActivities() not found; skipping URL activities');
113
        }
114
115
        // Gather activities (now includes URLs)
116
        $activities = $this->getActivities();
117
        @error_log('[MoodleExport::export] Activities count='.count($activities));
118
119
        // Export course structure (sections + activities metadata)
120
        $courseExport = new CourseExport($this->course, $activities);
121
        $courseExport->exportCourse($tempDir);
122
        @error_log('[MoodleExport::export] course/ exported');
123
124
        // Page export (collect extra files from HTML pages)
125
        $pageExport = new PageExport($this->course);
126
        $pageFiles = [];
127
        $pageData = $pageExport->getData(0, 1);
128
        if (!empty($pageData['files'])) {
129
            $pageFiles = $pageData['files'];
130
        }
131
        @error_log('[MoodleExport::export] pageFiles from PageExport='.count($pageFiles));
132
133
        // Files export (documents, attachments, + pages’ files)
134
        $fileExport = new FileExport($this->course);
135
        $filesData = $fileExport->getFilesData();
136
        @error_log('[MoodleExport::export] getFilesData='.count($filesData['files'] ?? []));
137
        $filesData['files'] = array_merge($filesData['files'] ?? [], $pageFiles);
138
        @error_log('[MoodleExport::export] merged files='.count($filesData['files'] ?? []));
139
        $fileExport->exportFiles($filesData, $tempDir);
140
141
        // Sections export (topics/weeks descriptors)
142
        $this->exportSections($tempDir);
143
        @error_log('[MoodleExport::export] sections/ exported');
144
145
        $this->exportLabelActivities($activities, $tempDir);
146
147
        // Root XMLs (course/activities indexes)
148
        $this->exportRootXmlFiles($tempDir);
149
        @error_log('[MoodleExport::export] root XMLs exported');
150
151
        // Create .mbz archive
152
        $exportedFile = $this->createMbzFile($tempDir);
153
        @error_log('[MoodleExport::export] mbz created at '.$exportedFile);
154
155
        // Cleanup temp dir
156
        $this->cleanupTempDir($tempDir);
157
        @error_log('[MoodleExport::export] tempDir removed '.$tempDir);
158
159
        @error_log('[MoodleExport::export] Done. file='.$exportedFile);
160
        return $exportedFile;
161
    }
162
163
    /**
164
     * Export questions data to XML file.
165
     */
166
    public function exportQuestionsXml(array $questionsData, string $exportDir): void
167
    {
168
        $quizExport = new QuizExport($this->course);
169
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
170
        $xmlContent .= '<question_categories>'.PHP_EOL;
171
172
        $categoryHashes = [];
173
        foreach ($questionsData as $quiz) {
174
            $categoryId = $quiz['questions'][0]['questioncategoryid'] ?? '1';
175
            $hash = md5($categoryId.($quiz['name'] ?? ''));
176
            if (isset($categoryHashes[$hash])) {
177
                continue;
178
            }
179
            $categoryHashes[$hash] = true;
180
            $xmlContent .= '  <question_category id="'.$categoryId.'">'.PHP_EOL;
181
            $xmlContent .= '    <name>Default for '.htmlspecialchars((string) $quiz['name'] ?? 'Unknown').'</name>'.PHP_EOL;
182
            $xmlContent .= '    <contextid>'.($quiz['contextid'] ?? '0').'</contextid>'.PHP_EOL;
183
            $xmlContent .= '    <contextlevel>70</contextlevel>'.PHP_EOL;
184
            $xmlContent .= '    <contextinstanceid>'.($quiz['moduleid'] ?? '0').'</contextinstanceid>'.PHP_EOL;
185
            $xmlContent .= '    <info>The default category for questions shared in context "'.htmlspecialchars($quiz['name'] ?? 'Unknown').'".</info>'.PHP_EOL;
186
            $xmlContent .= '    <infoformat>0</infoformat>'.PHP_EOL;
187
            $xmlContent .= '    <stamp>moodle+'.time().'+CATEGORYSTAMP</stamp>'.PHP_EOL;
188
            $xmlContent .= '    <parent>0</parent>'.PHP_EOL;
189
            $xmlContent .= '    <sortorder>999</sortorder>'.PHP_EOL;
190
            $xmlContent .= '    <idnumber>$@NULL@$</idnumber>'.PHP_EOL;
191
            $xmlContent .= '    <questions>'.PHP_EOL;
192
193
            foreach ($quiz['questions'] as $question) {
194
                $xmlContent .= $quizExport->exportQuestion($question);
195
            }
196
197
            $xmlContent .= '    </questions>'.PHP_EOL;
198
            $xmlContent .= '  </question_category>'.PHP_EOL;
199
        }
200
201
        $xmlContent .= '</question_categories>';
202
        file_put_contents($exportDir.'/questions.xml', $xmlContent);
203
    }
204
205
    /**
206
     * Sets the admin user data.
207
     */
208
    public function setAdminUserData(int $id, string $username, string $email): void
209
    {
210
        self::$adminUserData = [
211
            'id' => $id,
212
            'contextid' => $id,
213
            'username' => $username,
214
            'idnumber' => '',
215
            'email' => $email,
216
            'phone1' => '',
217
            'phone2' => '',
218
            'institution' => '',
219
            'department' => '',
220
            'address' => '',
221
            'city' => 'London',
222
            'country' => 'GB',
223
            'lastip' => '127.0.0.1',
224
            'picture' => '0',
225
            'description' => '',
226
            'descriptionformat' => 1,
227
            'imagealt' => '$@NULL@$',
228
            'auth' => 'manual',
229
            'firstname' => 'Admin',
230
            'lastname' => 'User',
231
            'confirmed' => 1,
232
            'policyagreed' => 0,
233
            'deleted' => 0,
234
            'lang' => 'en',
235
            'theme' => '',
236
            'timezone' => 99,
237
            'firstaccess' => time(),
238
            'lastaccess' => time() - (60 * 60 * 24 * 7),
239
            'lastlogin' => time() - (60 * 60 * 24 * 2),
240
            'currentlogin' => time(),
241
            'mailformat' => 1,
242
            'maildigest' => 0,
243
            'maildisplay' => 1,
244
            'autosubscribe' => 1,
245
            'trackforums' => 0,
246
            'timecreated' => time(),
247
            'timemodified' => time(),
248
            'trustbitmask' => 0,
249
            'preferences' => [
250
                ['name' => 'core_message_migrate_data', 'value' => 1],
251
                ['name' => 'auth_manual_passwordupdatetime', 'value' => time()],
252
                ['name' => 'email_bounce_count', 'value' => 1],
253
                ['name' => 'email_send_count', 'value' => 1],
254
                ['name' => 'login_failed_count_since_success', 'value' => 0],
255
                ['name' => 'filepicker_recentrepository', 'value' => 5],
256
                ['name' => 'filepicker_recentlicense', 'value' => 'unknown'],
257
            ],
258
        ];
259
    }
260
261
    /**
262
     * Returns hardcoded data for the admin user.
263
     *
264
     * @return array<string,mixed>
265
     */
266
    public static function getAdminUserData(): array
267
    {
268
        return self::$adminUserData;
269
    }
270
271
    public static function flagActivityUserinfo(string $modname, int $moduleId, bool $hasUserinfo): void
272
    {
273
        self::$activityUserinfo[$modname][$moduleId] = $hasUserinfo;
274
    }
275
276
    /**
277
     * Pulls dependent resources that LP items reference (only when LP bag exists).
278
     * Defensive: if no learnpath bag is present (e.g., exporting only documents),
279
     * this becomes a no-op. Keeps current behavior untouched when LP exist.
280
     */
281
    private function fillResourcesFromLearnpath(object $complete): void
282
    {
283
        // Accept both constant and plain-string keys defensively.
284
        $lpBag =
285
            $this->course->resources[\defined('RESOURCE_LEARNPATH') ? RESOURCE_LEARNPATH : 'learnpath']
286
            ?? $this->course->resources['learnpath']
287
            ?? [];
288
289
        if (empty($lpBag) || !\is_array($lpBag)) {
290
            // No learnpaths selected/present → nothing to hydrate.
291
            return;
292
        }
293
294
        foreach ($lpBag as $learnpathId => $learnpath) {
295
            // $learnpath may be wrapped in ->obj
296
            $lp = (\is_object($learnpath) && isset($learnpath->obj) && \is_object($learnpath->obj))
297
                ? $learnpath->obj
298
                : $learnpath;
299
300
            if (!\is_object($lp) || empty($lp->items) || !\is_array($lp->items)) {
301
                continue;
302
            }
303
304
            foreach ($lp->items as $item) {
305
                // Legacy LP items expose "item_type" and "path" (resource id)
306
                $type = $item['item_type'] ?? null;
307
                $resourceId = $item['path'] ?? null;
308
                if (!$type || null === $resourceId) {
309
                    continue;
310
                }
311
312
                // Bring missing deps from the complete snapshot (keeps old behavior when LP exist)
313
                if (isset($complete->resources[$type][$resourceId])
314
                    && !isset($this->course->resources[$type][$resourceId])) {
315
                    $this->course->resources[$type][$resourceId] = $complete->resources[$type][$resourceId];
316
                }
317
            }
318
        }
319
    }
320
321
    private function fillQuestionsFromQuiz(object $complete): void
322
    {
323
        if (!isset($this->course->resources['quiz'])) {
324
            return;
325
        }
326
        foreach ($this->course->resources['quiz'] as $quizId => $quiz) {
327
            if (!isset($quiz->obj->question_ids)) {
328
                continue;
329
            }
330
            foreach ($quiz->obj->question_ids as $questionId) {
331
                if (isset($complete->resources['Exercise_Question'][$questionId]) && !isset($this->course->resources['Exercise_Question'][$questionId])) {
332
                    $this->course->resources['Exercise_Question'][$questionId] = $complete->resources['Exercise_Question'][$questionId];
333
                }
334
            }
335
        }
336
    }
337
338
    private function exportRootXmlFiles(string $exportDir): void
339
    {
340
        $this->exportBadgesXml($exportDir);
341
        $this->exportCompletionXml($exportDir);
342
        $this->exportGradebookXml($exportDir);
343
        $this->exportGradeHistoryXml($exportDir);
344
        $this->exportGroupsXml($exportDir);
345
        $this->exportOutcomesXml($exportDir);
346
347
        $activities = $this->getActivities();
348
        $questionsData = [];
349
        foreach ($activities as $activity) {
350
            if ('quiz' === $activity['modulename']) {
351
                $quizExport = new QuizExport($this->course);
352
                $quizData = $quizExport->getData($activity['id'], $activity['sectionid']);
353
                $questionsData[] = $quizData;
354
            }
355
        }
356
        $this->exportQuestionsXml($questionsData, $exportDir);
357
358
        $this->exportRolesXml($exportDir);
359
        $this->exportScalesXml($exportDir);
360
        $this->exportUsersXml($exportDir);
361
    }
362
363
    private function createMoodleBackupXml(string $destinationDir, int $version): void
364
    {
365
        $courseInfo = api_get_course_info($this->course->code);
366
        $backupId = md5(bin2hex(random_bytes(16)));
367
        $siteHash = md5(bin2hex(random_bytes(16)));
368
        $wwwRoot = api_get_path(WEB_PATH);
369
370
        $courseStartDate = strtotime($courseInfo['creation_date']);
371
        $courseEndDate = $courseStartDate + (365 * 24 * 60 * 60);
372
373
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
374
        $xmlContent .= '<moodle_backup>'.PHP_EOL;
375
        $xmlContent .= '  <information>'.PHP_EOL;
376
377
        $xmlContent .= '    <name>backup-'.htmlspecialchars((string) $courseInfo['code']).'.mbz</name>'.PHP_EOL;
378
        $xmlContent .= '    <moodle_version>'.(3 === $version ? '2021051718' : '2022041900').'</moodle_version>'.PHP_EOL;
379
        $xmlContent .= '    <moodle_release>'.(3 === $version ? '3.11.18 (Build: 20231211)' : '4.x version here').'</moodle_release>'.PHP_EOL;
380
        $xmlContent .= '    <backup_version>'.(3 === $version ? '2021051700' : '2022041900').'</backup_version>'.PHP_EOL;
381
        $xmlContent .= '    <backup_release>'.(3 === $version ? '3.11' : '4.x').'</backup_release>'.PHP_EOL;
382
        $xmlContent .= '    <backup_date>'.time().'</backup_date>'.PHP_EOL;
383
        $xmlContent .= '    <mnet_remoteusers>0</mnet_remoteusers>'.PHP_EOL;
384
        $xmlContent .= '    <include_files>1</include_files>'.PHP_EOL;
385
        $xmlContent .= '    <include_file_references_to_external_content>0</include_file_references_to_external_content>'.PHP_EOL;
386
        $xmlContent .= '    <original_wwwroot>'.$wwwRoot.'</original_wwwroot>'.PHP_EOL;
387
        $xmlContent .= '    <original_site_identifier_hash>'.$siteHash.'</original_site_identifier_hash>'.PHP_EOL;
388
        $xmlContent .= '    <original_course_id>'.htmlspecialchars((string) $courseInfo['real_id']).'</original_course_id>'.PHP_EOL;
389
        $xmlContent .= '    <original_course_format>'.get_lang('Topics').'</original_course_format>'.PHP_EOL;
390
        $xmlContent .= '    <original_course_fullname>'.htmlspecialchars((string) $courseInfo['title']).'</original_course_fullname>'.PHP_EOL;
391
        $xmlContent .= '    <original_course_shortname>'.htmlspecialchars((string) $courseInfo['code']).'</original_course_shortname>'.PHP_EOL;
392
        $xmlContent .= '    <original_course_startdate>'.$courseStartDate.'</original_course_startdate>'.PHP_EOL;
393
        $xmlContent .= '    <original_course_enddate>'.$courseEndDate.'</original_course_enddate>'.PHP_EOL;
394
        $xmlContent .= '    <original_course_contextid>'.$courseInfo['real_id'].'</original_course_contextid>'.PHP_EOL;
395
        $xmlContent .= '    <original_system_contextid>'.api_get_current_access_url_id().'</original_system_contextid>'.PHP_EOL;
396
397
        $xmlContent .= '    <details>'.PHP_EOL;
398
        $xmlContent .= '      <detail backup_id="'.$backupId.'">'.PHP_EOL;
399
        $xmlContent .= '        <type>course</type>'.PHP_EOL;
400
        $xmlContent .= '        <format>moodle2</format>'.PHP_EOL;
401
        $xmlContent .= '        <interactive>1</interactive>'.PHP_EOL;
402
        $xmlContent .= '        <mode>10</mode>'.PHP_EOL;
403
        $xmlContent .= '        <execution>1</execution>'.PHP_EOL;
404
        $xmlContent .= '        <executiontime>0</executiontime>'.PHP_EOL;
405
        $xmlContent .= '      </detail>'.PHP_EOL;
406
        $xmlContent .= '    </details>'.PHP_EOL;
407
408
        $xmlContent .= '    <contents>'.PHP_EOL;
409
410
        $sections = $this->getSections();
411
        if (!empty($sections)) {
412
            $xmlContent .= '      <sections>'.PHP_EOL;
413
            foreach ($sections as $section) {
414
                $xmlContent .= '        <section>'.PHP_EOL;
415
                $xmlContent .= '          <sectionid>'.$section['id'].'</sectionid>'.PHP_EOL;
416
                $xmlContent .= '          <title>'.htmlspecialchars((string) $section['name']).'</title>'.PHP_EOL;
417
                $xmlContent .= '          <directory>sections/section_'.$section['id'].'</directory>'.PHP_EOL;
418
                $xmlContent .= '        </section>'.PHP_EOL;
419
            }
420
            $xmlContent .= '      </sections>'.PHP_EOL;
421
        }
422
423
        $seenActs = [];
424
        $activitiesFlat = [];
425
        foreach ($sections as $section) {
426
            foreach ($section['activities'] as $a) {
427
                $modname = (string) ($a['modulename'] ?? '');
428
                $moduleid = isset($a['moduleid']) ? (int) $a['moduleid'] : null;
429
                if ('' === $modname || null === $moduleid || $moduleid < 0) {
430
                    continue;
431
                }
432
                $key = $modname.':'.$moduleid;
433
                if (isset($seenActs[$key])) {
434
                    continue;
435
                }
436
                $seenActs[$key] = true;
437
438
                $title = (string) ($a['title'] ?? $a['name'] ?? '');
439
                $activitiesFlat[] = [
440
                    'moduleid' => $moduleid,
441
                    'sectionid' => (int) $section['id'],
442
                    'modulename' => $modname,
443
                    'title' => $title,
444
                ];
445
            }
446
        }
447
448
        // Append only "label" activities discovered by getActivities(), with dedupe
449
        foreach ($this->getActivities() as $a) {
450
            $modname  = (string) ($a['modulename'] ?? '');
451
            if ($modname !== 'label') {
452
                continue; // keep minimal: only labels are missing in backup XML
453
            }
454
455
            $moduleid = (int) ($a['moduleid'] ?? 0);
456
            if ($moduleid <= 0) {
457
                continue;
458
            }
459
460
            $key = $modname.':'.$moduleid;
461
            if (isset($seenActs[$key])) {
462
                continue; // already present via sections, skip to avoid duplicates
463
            }
464
            $seenActs[$key] = true;
465
466
            // Ensure we propagate title and section for the backup XML
467
            $activitiesFlat[] = [
468
                'moduleid'  => $moduleid,
469
                'sectionid' => (int) ($a['sectionid'] ?? 0),
470
                'modulename'=> 'label',
471
                'title'     => (string) ($a['title'] ?? ''),
472
            ];
473
        }
474
475
        if (!empty($activitiesFlat)) {
476
            $xmlContent .= '      <activities>'.PHP_EOL;
477
            foreach ($activitiesFlat as $activity) {
478
                $modname  = (string) $activity['modulename'];
479
                $moduleid = (int)    $activity['moduleid'];
480
                $sectionid= (int)    $activity['sectionid'];
481
                $title    = (string) $activity['title'];
482
483
                $hasUserinfo = self::$activityUserinfo[$modname][$moduleid] ?? false;
484
485
                $xmlContent .= '        <activity>'.PHP_EOL;
486
                $xmlContent .= '          <moduleid>'.$moduleid.'</moduleid>'.PHP_EOL;
487
                $xmlContent .= '          <sectionid>'.$sectionid.'</sectionid>'.PHP_EOL;
488
                $xmlContent .= '          <modulename>'.htmlspecialchars($modname).'</modulename>'.PHP_EOL;
489
                $xmlContent .= '          <title>'.htmlspecialchars($title).'</title>'.PHP_EOL;
490
                $xmlContent .= '          <directory>activities/'.$modname.'_'.$moduleid.'</directory>'.PHP_EOL;
491
                $xmlContent .= '          <userinfo>'.($hasUserinfo ? '1' : '0').'</userinfo>'.PHP_EOL;
492
                $xmlContent .= '        </activity>'.PHP_EOL;
493
            }
494
            $xmlContent .= '      </activities>'.PHP_EOL;
495
        }
496
497
        $xmlContent .= '      <course>'.PHP_EOL;
498
        $xmlContent .= '        <courseid>'.$courseInfo['real_id'].'</courseid>'.PHP_EOL;
499
        $xmlContent .= '        <title>'.htmlspecialchars((string) $courseInfo['title']).'</title>'.PHP_EOL;
500
        $xmlContent .= '        <directory>course</directory>'.PHP_EOL;
501
        $xmlContent .= '      </course>'.PHP_EOL;
502
503
        $xmlContent .= '    </contents>'.PHP_EOL;
504
505
        $xmlContent .= '    <settings>'.PHP_EOL;
506
        $activities = $activitiesFlat;
507
        $settings = $this->exportBackupSettings($sections, $activities);
508
        foreach ($settings as $setting) {
509
            $xmlContent .= '      <setting>'.PHP_EOL;
510
            $xmlContent .= '        <level>'.htmlspecialchars($setting['level']).'</level>'.PHP_EOL;
511
            $xmlContent .= '        <name>'.htmlspecialchars($setting['name']).'</name>'.PHP_EOL;
512
            $xmlContent .= '        <value>'.$setting['value'].'</value>'.PHP_EOL;
513
            if (isset($setting['section'])) {
514
                $xmlContent .= '        <section>'.htmlspecialchars($setting['section']).'</section>'.PHP_EOL;
515
            }
516
            if (isset($setting['activity'])) {
517
                $xmlContent .= '        <activity>'.htmlspecialchars($setting['activity']).'</activity>'.PHP_EOL;
518
            }
519
            $xmlContent .= '      </setting>'.PHP_EOL;
520
        }
521
        $xmlContent .= '    </settings>'.PHP_EOL;
522
523
        $xmlContent .= '  </information>'.PHP_EOL;
524
        $xmlContent .= '</moodle_backup>';
525
526
        $xmlFile = $destinationDir.'/moodle_backup.xml';
527
        file_put_contents($xmlFile, $xmlContent);
528
    }
529
530
    /**
531
     * Builds the sections array for moodle_backup.xml and for sections/* export.
532
     * Defensive: if no learnpaths are present/selected, only "General" (section 0) is emitted.
533
     * When LP exist, behavior remains unchanged.
534
     */
535
    private function getSections(): array
536
    {
537
        $sectionExport = new SectionExport($this->course);
538
        $sections = [];
539
540
        // Resolve LP bag defensively (constant or string key; or none)
541
        $lpBag =
542
            $this->course->resources[\defined('RESOURCE_LEARNPATH') ? RESOURCE_LEARNPATH : 'learnpath']
543
            ?? $this->course->resources['learnpath']
544
            ?? [];
545
546
        if (!empty($lpBag) && \is_array($lpBag)) {
547
            foreach ($lpBag as $learnpath) {
548
                // Unwrap if needed
549
                $lp = (\is_object($learnpath) && isset($learnpath->obj) && \is_object($learnpath->obj))
550
                    ? $learnpath->obj
551
                    : $learnpath;
552
553
                // Some exports use string '1' or int 1 for LP type = learnpath
554
                $lpType = \is_object($lp) && isset($lp->lp_type) ? (string) $lp->lp_type : '';
555
                if ('1' === $lpType) {
556
                    $sections[] = $sectionExport->getSectionData($learnpath);
557
                }
558
            }
559
        }
560
561
        // Always add "General" (section 0)
562
        $sections[] = [
563
            'id' => 0,
564
            'number' => 0,
565
            'name' => get_lang('General'),
566
            'summary' => get_lang('GeneralResourcesCourse'),
567
            'sequence' => 0,
568
            'visible' => 1,
569
            'timemodified' => time(),
570
            'activities' => $sectionExport->getActivitiesForGeneral(),
571
        ];
572
573
        return $sections;
574
    }
575
576
    private function getActivities(): array
577
    {
578
        @error_log('[MoodleExport::getActivities] Start');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for error_log(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

578
        /** @scrutinizer ignore-unhandled */ @error_log('[MoodleExport::getActivities] Start');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
579
580
        $activities = [];
581
        $glossaryAdded = false;
582
583
        // Build a "documents" bucket (root-level files/folders)
584
        $docBucket = [];
585
        if (\defined('RESOURCE_DOCUMENT') && isset($this->course->resources[RESOURCE_DOCUMENT]) && \is_array($this->course->resources[RESOURCE_DOCUMENT])) {
586
            $docBucket = $this->course->resources[RESOURCE_DOCUMENT];
587
        } elseif (isset($this->course->resources['document']) && \is_array($this->course->resources['document'])) {
588
            $docBucket = $this->course->resources['document'];
589
        }
590
        @error_log('[MoodleExport::getActivities] docBucket='.count($docBucket));
591
592
        // Add a visible "Documents" folder activity if we actually have documents
593
        if (!empty($docBucket)) {
594
            $activities[] = [
595
                'id'        => ActivityExport::DOCS_MODULE_ID,
596
                'sectionid' => 0,
597
                'modulename'=> 'folder',
598
                'moduleid'  => ActivityExport::DOCS_MODULE_ID,
599
                'title'     => 'Documents',
600
            ];
601
            @error_log('[MoodleExport::getActivities] Added visible folder activity "Documents" (moduleid=' . ActivityExport::DOCS_MODULE_ID . ').');
602
        }
603
604
        $htmlPageIds = [];
605
606
        foreach ($this->course->resources as $resourceType => $resources) {
607
            if (!\is_array($resources) || empty($resources)) {
608
                continue;
609
            }
610
611
            foreach ($resources as $resource) {
612
                $exportClass = null;
613
                $moduleName = '';
614
                $title = '';
615
                $id = 0;
616
617
                // Quiz
618
                if (RESOURCE_QUIZ === $resourceType && ($resource->obj->iid ?? 0) > 0) {
619
                    $exportClass = QuizExport::class;
620
                    $moduleName = 'quiz';
621
                    $id = (int) $resource->obj->iid;
622
                    $title = (string) $resource->obj->title;
623
                }
624
625
                // URL
626
                if (RESOURCE_LINK === $resourceType && ($resource->source_id ?? 0) > 0) {
627
                    $exportClass = UrlExport::class;
628
                    $moduleName = 'url';
629
                    $id = (int) $resource->source_id;
630
                    $title = (string) ($resource->title ?? '');
631
                }
632
                // Glossary (only once)
633
                elseif (RESOURCE_GLOSSARY === $resourceType && ($resource->glossary_id ?? 0) > 0 && !$glossaryAdded) {
634
                    $exportClass = GlossaryExport::class;
635
                    $moduleName = 'glossary';
636
                    $id = 1;
637
                    $title = get_lang('Glossary');
638
                    $glossaryAdded = true;
639
                }
640
                // Forum
641
                elseif (RESOURCE_FORUM === $resourceType && ($resource->source_id ?? 0) > 0) {
642
                    $exportClass = ForumExport::class;
643
                    $moduleName = 'forum';
644
                    $id = (int) ($resource->obj->iid ?? 0);
645
                    $title = (string) ($resource->obj->forum_title ?? '');
646
                }
647
                // Documents (as Page or Resource)
648
                elseif (RESOURCE_DOCUMENT === $resourceType && ($resource->source_id ?? 0) > 0) {
649
                    $resPath = (string) ($resource->path ?? '');
650
                    $resTitle = (string) ($resource->title ?? '');
651
                    $fileType = (string) ($resource->file_type ?? '');
652
653
                    $isRoot = ('' !== $resPath && 1 === substr_count($resPath, '/'));
654
                    $ext = '' !== $resPath ? pathinfo($resPath, PATHINFO_EXTENSION) : '';
655
656
                    // Root HTML -> export as "page"
657
                    if ('html' === $ext && $isRoot) {
658
                        $exportClass = PageExport::class;
659
                        $moduleName = 'page';
660
                        $id = (int) $resource->source_id;
661
                        $title = $resTitle;
662
                        $htmlPageIds[] = $id;
663
                    }
664
665
                    // Regular file -> export as "resource" (avoid colliding with pages)
666
                    if ('file' === $fileType && !\in_array($resource->source_id, $htmlPageIds, true)) {
667
                        $resourceExport = new ResourceExport($this->course);
668
                        if ($resourceExport->getSectionIdForActivity((int) $resource->source_id, $resourceType) > 0) {
669
                            if ($isRoot) {
670
                                $exportClass = ResourceExport::class;
671
                                $moduleName = 'resource';
672
                                $id = (int) $resource->source_id;
673
                                $title = '' !== $resTitle ? $resTitle : (basename($resPath) ?: ('File '.$id));
674
                            }
675
                        }
676
                    }
677
                }
678
                // *** Tool Intro -> treat "course_homepage" as a Page activity (id=0) ***
679
                elseif (RESOURCE_TOOL_INTRO === $resourceType) {
680
                    // IMPORTANT: do not check source_id; the real key is obj->id
681
                    $objId = (string) ($resource->obj->id ?? '');
682
                    if ($objId === 'course_homepage') {
683
                        $exportClass = PageExport::class;
684
                        $moduleName = 'page';
685
                        // Keep activity id = 0 → PageExport::getData(0, ...) reads the intro HTML
686
                        $id = 0;
687
                        $title = get_lang('Introduction');
688
                    }
689
                }
690
                // Assignments
691
                elseif (RESOURCE_WORK === $resourceType && ($resource->source_id ?? 0) > 0) {
692
                    $exportClass = AssignExport::class;
693
                    $moduleName = 'assign';
694
                    $id = (int) $resource->source_id;
695
                    $title = (string) ($resource->params['title'] ?? '');
696
                }
697
                // Surveys -> Feedback
698
                elseif (RESOURCE_SURVEY === $resourceType && ($resource->source_id ?? 0) > 0) {
699
                    $exportClass = FeedbackExport::class;
700
                    $moduleName = 'feedback';
701
                    $id = (int) $resource->source_id;
702
                    $title = (string) ($resource->params['title'] ?? '');
703
                }
704
                // Course descriptions
705
                elseif (RESOURCE_COURSEDESCRIPTION === $resourceType && ($resource->source_id ?? 0) > 0) {
706
                    $exportClass = LabelExport::class;
707
                    $moduleName  = 'label';
708
                    $id          = (int) $resource->source_id;
709
                    $title       = (string) ($resource->title ?? '');
710
                }
711
712
                // Emit activity if resolved
713
                if ($exportClass && $moduleName) {
714
                    /** @var object $exportInstance */
715
                    $exportInstance = new $exportClass($this->course);
716
                    $sectionId = $exportInstance->getSectionIdForActivity($id, $resourceType);
717
                    $activities[] = [
718
                        'id' => $id,
719
                        'sectionid' => $sectionId,
720
                        'modulename' => $moduleName,
721
                        'moduleid' => $id,
722
                        'title' => $title,
723
                    ];
724
                    @error_log('[MoodleExport::getActivities] ADD modulename='.$moduleName.' moduleid='.$id.' sectionid='.$sectionId.' title="'.str_replace(["\n","\r"],' ',$title).'"');
725
                }
726
            }
727
        }
728
729
        @error_log('[MoodleExport::getActivities] Done. total='.count($activities));
730
        return $activities;
731
    }
732
733
    /**
734
     * Collect Moodle URL activities from legacy "link" bucket.
735
     *
736
     * It is defensive against different wrappers:
737
     * - Accepts link objects as $wrap->obj or directly as $wrap.
738
     * - Resolves title from title|name|url (last-resort).
739
     * - Maps category_id to a section name (category title) if available.
740
     *
741
     * @return UrlExport[]
742
     */
743
    private function buildUrlActivities(): array
744
    {
745
        $res = \is_array($this->course->resources ?? null) ? $this->course->resources : [];
746
747
        // Buckets (defensive: allow legacy casings)
748
        $links = $res['link'] ?? $res['Link'] ?? [];
749
        $cats  = $res['link_category'] ?? $res['Link_Category'] ?? [];
750
751
        // Map category_id → label for section naming
752
        $catLabel = [];
753
        foreach ($cats as $cid => $cwrap) {
754
            if (!\is_object($cwrap)) {
755
                continue;
756
            }
757
            $c = (isset($cwrap->obj) && \is_object($cwrap->obj)) ? $cwrap->obj : $cwrap;
758
            $label = '';
759
            foreach (['title', 'name'] as $k) {
760
                if (!empty($c->{$k}) && \is_string($c->{$k})) {
761
                    $label = trim((string) $c->{$k});
762
                    break;
763
                }
764
            }
765
            $catLabel[(int) $cid] = $label !== '' ? $label : ('Category #'.(int) $cid);
766
        }
767
768
        $out = [];
769
        foreach ($links as $id => $lwrap) {
770
            if (!\is_object($lwrap)) {
771
                continue;
772
            }
773
            $L = (isset($lwrap->obj) && \is_object($lwrap->obj)) ? $lwrap->obj : $lwrap;
774
775
            $url = (string) ($L->url ?? '');
776
            if ($url === '') {
777
                // Skip invalid URL records
778
                continue;
779
            }
780
781
            // Resolve a robust title
782
            $title = '';
783
            foreach (['title', 'name'] as $k) {
784
                if (!empty($L->{$k}) && \is_string($L->{$k})) {
785
                    $title = trim((string) $L->{$k});
786
                    break;
787
                }
788
            }
789
            if ($title === '') {
790
                $title = $url; // last resort: use the URL itself
791
            }
792
793
            $target = (string) ($L->target ?? '');
794
            $intro  = (string) ($L->description ?? '');
795
            $cid    = (int) ($L->category_id ?? 0);
796
797
            $sectionName = $catLabel[$cid] ?? null;
798
799
            // UrlExport ctor: (string $title, string $url, ?string $section = null, ?string $introHtml = null, ?string $target = null)
800
            $urlAct = new UrlExport($title, $url, $sectionName ?: null, $intro ?: null, $target ?: null);
801
            if (method_exists($urlAct, 'setLegacyId')) {
802
                $urlAct->setLegacyId((int) $id);
803
            }
804
805
            $out[] = $urlAct;
806
        }
807
808
        return $out;
809
    }
810
811
    /**
812
     * Enqueue all URL activities into the export pipeline.
813
     * Will try queueActivity(), then addActivity(), then $this->activities[].
814
     */
815
    private function enqueueUrlActivities(): void
816
    {
817
        $urls = $this->buildUrlActivities();
818
819
        if (empty($urls)) {
820
            @error_log('[MoodleExport] No URL activities to enqueue');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for error_log(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

820
            /** @scrutinizer ignore-unhandled */ @error_log('[MoodleExport] No URL activities to enqueue');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
821
            return;
822
        }
823
824
        if (method_exists($this, 'queueActivity')) {
825
            foreach ($urls as $act) {
826
                $this->queueActivity($act);
827
            }
828
            @error_log('[MoodleExport] URL activities enqueued via queueActivity(): '.count($urls));
829
            return;
830
        }
831
832
        if (method_exists($this, 'addActivity')) {
833
            foreach ($urls as $act) {
834
                $this->addActivity($act);
835
            }
836
            @error_log('[MoodleExport] URL activities appended via addActivity(): '.count($urls));
837
            return;
838
        }
839
840
        if (property_exists($this, 'activities') && \is_array($this->activities)) {
841
            array_push($this->activities, ...$urls);
842
            @error_log('[MoodleExport] URL activities appended to $this->activities: '.count($urls));
843
            return;
844
        }
845
846
        @error_log('[MoodleExport][WARN] Could not enqueue URL activities (no compatible method found)');
847
    }
848
849
    private function exportSections(string $exportDir): void
850
    {
851
        $sections = $this->getSections();
852
        foreach ($sections as $section) {
853
            $sectionExport = new SectionExport($this->course);
854
            $sectionExport->exportSection($section['id'], $exportDir);
855
        }
856
    }
857
858
    private function createMbzFile(string $sourceDir): string
859
    {
860
        $zip = new ZipArchive();
861
        $zipFile = $sourceDir.'.mbz';
862
863
        if (true !== $zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE)) {
864
            throw new Exception(get_lang('ErrorCreatingZip'));
865
        }
866
867
        $files = new RecursiveIteratorIterator(
868
            new RecursiveDirectoryIterator($sourceDir),
869
            RecursiveIteratorIterator::LEAVES_ONLY
870
        );
871
872
        foreach ($files as $file) {
873
            if (!$file->isDir()) {
874
                $filePath = $file->getRealPath();
875
                $relativePath = substr($filePath, \strlen($sourceDir) + 1);
876
877
                if (!$zip->addFile($filePath, $relativePath)) {
878
                    throw new Exception(get_lang('ErrorAddingFileToZip').": $relativePath");
879
                }
880
            }
881
        }
882
883
        if (!$zip->close()) {
884
            throw new Exception(get_lang('ErrorClosingZip'));
885
        }
886
887
        return $zipFile;
888
    }
889
890
    private function cleanupTempDir(string $dir): void
891
    {
892
        $this->recursiveDelete($dir);
893
    }
894
895
    private function recursiveDelete(string $dir): void
896
    {
897
        $files = array_diff(scandir($dir), ['.', '..']);
898
        foreach ($files as $file) {
899
            $path = "$dir/$file";
900
            is_dir($path) ? $this->recursiveDelete($path) : unlink($path);
901
        }
902
        rmdir($dir);
903
    }
904
905
    /**
906
     * Export Label activities into activities/label_{id}/label.xml
907
     * Keeps getActivities() side-effect free.
908
     */
909
    private function exportLabelActivities(array $activities, string $exportDir): void
910
    {
911
        foreach ($activities as $a) {
912
            if (($a['modulename'] ?? '') !== 'label') {
913
                continue;
914
            }
915
            try {
916
                $label     = new LabelExport($this->course);
917
                $activityId= (int) $a['id'];
918
                $moduleId  = (int) $a['moduleid'];
919
                $sectionId = (int) $a['sectionid'];
920
921
                // Correct argument order: (activityId, exportDir, moduleId, sectionId)
922
                $label->export($activityId, $exportDir, $moduleId, $sectionId);
923
924
                @error_log('[MoodleExport::exportLabelActivities] exported label moduleid='.$moduleId.' sectionid='.$sectionId);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for error_log(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

924
                /** @scrutinizer ignore-unhandled */ @error_log('[MoodleExport::exportLabelActivities] exported label moduleid='.$moduleId.' sectionid='.$sectionId);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
925
            } catch (\Throwable $e) {
926
                @error_log('[MoodleExport::exportLabelActivities][ERROR] '.$e->getMessage());
927
            }
928
        }
929
    }
930
931
    private function exportBadgesXml(string $exportDir): void
932
    {
933
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
934
        $xmlContent .= '<badges>'.PHP_EOL;
935
        $xmlContent .= '</badges>';
936
        file_put_contents($exportDir.'/badges.xml', $xmlContent);
937
    }
938
939
    private function exportCompletionXml(string $exportDir): void
940
    {
941
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
942
        $xmlContent .= '<completions>'.PHP_EOL;
943
        $xmlContent .= '</completions>';
944
        file_put_contents($exportDir.'/completion.xml', $xmlContent);
945
    }
946
947
    private function exportGradebookXml(string $exportDir): void
948
    {
949
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
950
        $xmlContent .= '<gradebook>'.PHP_EOL;
951
        $xmlContent .= '</gradebook>';
952
        file_put_contents($exportDir.'/gradebook.xml', $xmlContent);
953
    }
954
955
    private function exportGradeHistoryXml(string $exportDir): void
956
    {
957
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
958
        $xmlContent .= '<grade_history>'.PHP_EOL;
959
        $xmlContent .= '</grade_history>';
960
        file_put_contents($exportDir.'/grade_history.xml', $xmlContent);
961
    }
962
963
    private function exportGroupsXml(string $exportDir): void
964
    {
965
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
966
        $xmlContent .= '<groups>'.PHP_EOL;
967
        $xmlContent .= '</groups>';
968
        file_put_contents($exportDir.'/groups.xml', $xmlContent);
969
    }
970
971
    private function exportOutcomesXml(string $exportDir): void
972
    {
973
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
974
        $xmlContent .= '<outcomes>'.PHP_EOL;
975
        $xmlContent .= '</outcomes>';
976
        file_put_contents($exportDir.'/outcomes.xml', $xmlContent);
977
    }
978
979
    private function exportRolesXml(string $exportDir): void
980
    {
981
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
982
        $xmlContent .= '<roles_definition>'.PHP_EOL;
983
        $xmlContent .= '  <role id="5">'.PHP_EOL;
984
        $xmlContent .= '    <name></name>'.PHP_EOL;
985
        $xmlContent .= '    <shortname>student</shortname>'.PHP_EOL;
986
        $xmlContent .= '    <nameincourse>$@NULL@$</nameincourse>'.PHP_EOL;
987
        $xmlContent .= '    <description></description>'.PHP_EOL;
988
        $xmlContent .= '    <sortorder>5</sortorder>'.PHP_EOL;
989
        $xmlContent .= '    <archetype>student</archetype>'.PHP_EOL;
990
        $xmlContent .= '  </role>'.PHP_EOL;
991
        $xmlContent .= '</roles_definition>'.PHP_EOL;
992
993
        file_put_contents($exportDir.'/roles.xml', $xmlContent);
994
    }
995
996
    private function exportScalesXml(string $exportDir): void
997
    {
998
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
999
        $xmlContent .= '<scales>'.PHP_EOL;
1000
        $xmlContent .= '</scales>';
1001
        file_put_contents($exportDir.'/scales.xml', $xmlContent);
1002
    }
1003
1004
    private function exportUsersXml(string $exportDir): void
1005
    {
1006
        $adminData = self::getAdminUserData();
1007
1008
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
1009
        $xmlContent .= '<users>'.PHP_EOL;
1010
        $xmlContent .= '  <user id="'.$adminData['id'].'" contextid="'.$adminData['contextid'].'">'.PHP_EOL;
1011
        $xmlContent .= '    <username>'.$adminData['username'].'</username>'.PHP_EOL;
1012
        $xmlContent .= '    <idnumber>'.$adminData['idnumber'].'</idnumber>'.PHP_EOL;
1013
        $xmlContent .= '    <email>'.$adminData['email'].'</email>'.PHP_EOL;
1014
        $xmlContent .= '    <phone1>'.$adminData['phone1'].'</phone1>'.PHP_EOL;
1015
        $xmlContent .= '    <phone2>'.$adminData['phone2'].'</phone2>'.PHP_EOL;
1016
        $xmlContent .= '    <institution>'.$adminData['institution'].'</institution>'.PHP_EOL;
1017
        $xmlContent .= '    <department>'.$adminData['department'].'</department>'.PHP_EOL;
1018
        $xmlContent .= '    <address>'.$adminData['address'].'</address>'.PHP_EOL;
1019
        $xmlContent .= '    <city>'.$adminData['city'].'</city>'.PHP_EOL;
1020
        $xmlContent .= '    <country>'.$adminData['country'].'</country>'.PHP_EOL;
1021
        $xmlContent .= '    <lastip>'.$adminData['lastip'].'</lastip>'.PHP_EOL;
1022
        $xmlContent .= '    <picture>'.$adminData['picture'].'</picture>'.PHP_EOL;
1023
        $xmlContent .= '    <description>'.$adminData['description'].'</description>'.PHP_EOL;
1024
        $xmlContent .= '    <descriptionformat>'.$adminData['descriptionformat'].'</descriptionformat>'.PHP_EOL;
1025
        $xmlContent .= '    <imagealt>'.$adminData['imagealt'].'</imagealt>'.PHP_EOL;
1026
        $xmlContent .= '    <auth>'.$adminData['auth'].'</auth>'.PHP_EOL;
1027
        $xmlContent .= '    <firstname>'.$adminData['firstname'].'</firstname>'.PHP_EOL;
1028
        $xmlContent .= '    <lastname>'.$adminData['lastname'].'</lastname>'.PHP_EOL;
1029
        $xmlContent .= '    <confirmed>'.$adminData['confirmed'].'</confirmed>'.PHP_EOL;
1030
        $xmlContent .= '    <policyagreed>'.$adminData['policyagreed'].'</policyagreed>'.PHP_EOL;
1031
        $xmlContent .= '    <deleted>'.$adminData['deleted'].'</deleted>'.PHP_EOL;
1032
        $xmlContent .= '    <lang>'.$adminData['lang'].'</lang>'.PHP_EOL;
1033
        $xmlContent .= '    <theme>'.$adminData['theme'].'</theme>'.PHP_EOL;
1034
        $xmlContent .= '    <timezone>'.$adminData['timezone'].'</timezone>'.PHP_EOL;
1035
        $xmlContent .= '    <firstaccess>'.$adminData['firstaccess'].'</firstaccess>'.PHP_EOL;
1036
        $xmlContent .= '    <lastaccess>'.$adminData['lastaccess'].'</lastaccess>'.PHP_EOL;
1037
        $xmlContent .= '    <lastlogin>'.$adminData['lastlogin'].'</lastlogin>'.PHP_EOL;
1038
        $xmlContent .= '    <currentlogin>'.$adminData['currentlogin'].'</currentlogin>'.PHP_EOL;
1039
        $xmlContent .= '    <mailformat>'.$adminData['mailformat'].'</mailformat>'.PHP_EOL;
1040
        $xmlContent .= '    <maildigest>'.$adminData['maildigest'].'</maildigest>'.PHP_EOL;
1041
        $xmlContent .= '    <maildisplay>'.$adminData['maildisplay'].'</maildisplay>'.PHP_EOL;
1042
        $xmlContent .= '    <autosubscribe>'.$adminData['autosubscribe'].'</autosubscribe>'.PHP_EOL;
1043
        $xmlContent .= '    <trackforums>'.$adminData['trackforums'].'</trackforums>'.PHP_EOL;
1044
        $xmlContent .= '    <timecreated>'.$adminData['timecreated'].'</timecreated>'.PHP_EOL;
1045
        $xmlContent .= '    <timemodified>'.$adminData['timemodified'].'</timemodified>'.PHP_EOL;
1046
        $xmlContent .= '    <trustbitmask>'.$adminData['trustbitmask'].'</trustbitmask>'.PHP_EOL;
1047
1048
        if (isset($adminData['preferences']) && \is_array($adminData['preferences'])) {
1049
            $xmlContent .= '    <preferences>'.PHP_EOL;
1050
            foreach ($adminData['preferences'] as $preference) {
1051
                $xmlContent .= '      <preference>'.PHP_EOL;
1052
                $xmlContent .= '        <name>'.htmlspecialchars((string) $preference['name']).'</name>'.PHP_EOL;
1053
                $xmlContent .= '        <value>'.htmlspecialchars((string) $preference['value']).'</value>'.PHP_EOL;
1054
                $xmlContent .= '      </preference>'.PHP_EOL;
1055
            }
1056
            $xmlContent .= '    </preferences>'.PHP_EOL;
1057
        } else {
1058
            $xmlContent .= '    <preferences></preferences>'.PHP_EOL;
1059
        }
1060
1061
        $xmlContent .= '    <roles>'.PHP_EOL;
1062
        $xmlContent .= '      <role_overrides></role_overrides>'.PHP_EOL;
1063
        $xmlContent .= '      <role_assignments></role_assignments>'.PHP_EOL;
1064
        $xmlContent .= '    </roles>'.PHP_EOL;
1065
1066
        $xmlContent .= '  </user>'.PHP_EOL;
1067
        $xmlContent .= '</users>';
1068
1069
        file_put_contents($exportDir.'/users.xml', $xmlContent);
1070
    }
1071
1072
    private function exportBackupSettings(array $sections, array $activities): array
1073
    {
1074
        $settings = [
1075
            ['level' => 'root', 'name' => 'filename', 'value' => 'backup-moodle-course-'.time().'.mbz'],
1076
            ['level' => 'root', 'name' => 'imscc11', 'value' => '0'],
1077
            ['level' => 'root', 'name' => 'users', 'value' => '1'],
1078
            ['level' => 'root', 'name' => 'anonymize', 'value' => '0'],
1079
            ['level' => 'root', 'name' => 'role_assignments', 'value' => '1'],
1080
            ['level' => 'root', 'name' => 'activities', 'value' => '1'],
1081
            ['level' => 'root', 'name' => 'blocks', 'value' => '1'],
1082
            ['level' => 'root', 'name' => 'files', 'value' => '1'],
1083
            ['level' => 'root', 'name' => 'filters', 'value' => '1'],
1084
            ['level' => 'root', 'name' => 'comments', 'value' => '1'],
1085
            ['level' => 'root', 'name' => 'badges', 'value' => '1'],
1086
            ['level' => 'root', 'name' => 'calendarevents', 'value' => '1'],
1087
            ['level' => 'root', 'name' => 'userscompletion', 'value' => '1'],
1088
            ['level' => 'root', 'name' => 'logs', 'value' => '0'],
1089
            ['level' => 'root', 'name' => 'grade_histories', 'value' => '0'],
1090
            ['level' => 'root', 'name' => 'questionbank', 'value' => '1'],
1091
            ['level' => 'root', 'name' => 'groups', 'value' => '1'],
1092
            ['level' => 'root', 'name' => 'competencies', 'value' => '0'],
1093
            ['level' => 'root', 'name' => 'customfield', 'value' => '1'],
1094
            ['level' => 'root', 'name' => 'contentbankcontent', 'value' => '1'],
1095
            ['level' => 'root', 'name' => 'legacyfiles', 'value' => '1'],
1096
        ];
1097
1098
        foreach ($sections as $section) {
1099
            $settings[] = [
1100
                'level' => 'section',
1101
                'section' => 'section_'.$section['id'],
1102
                'name' => 'section_'.$section['id'].'_included',
1103
                'value' => '1',
1104
            ];
1105
            $settings[] = [
1106
                'level' => 'section',
1107
                'section' => 'section_'.$section['id'],
1108
                'name' => 'section_'.$section['id'].'_userinfo',
1109
                'value' => '1',
1110
            ];
1111
        }
1112
1113
        foreach ($activities as $activity) {
1114
            $settings[] = [
1115
                'level' => 'activity',
1116
                'activity' => $activity['modulename'].'_'.$activity['moduleid'],
1117
                'name' => $activity['modulename'].'_'.$activity['moduleid'].'_included',
1118
                'value' => '1',
1119
            ];
1120
            $value = (self::$activityUserinfo[$activity['modulename']][$activity['moduleid']] ?? false) ? '1' : '0';
1121
            $settings[] = [
1122
                'level' => 'activity',
1123
                'activity' => $activity['modulename'].'_'.$activity['moduleid'],
1124
                'name' => $activity['modulename'].'_'.$activity['moduleid'].'_userinfo',
1125
                'value' => $value,
1126
            ];
1127
        }
1128
1129
        return $settings;
1130
    }
1131
}
1132