Passed
Pull Request — master (#6935)
by
unknown
08:44
created

MoodleExport::buildUrlActivities()   F

Complexity

Conditions 23
Paths 2808

Size

Total Lines 66
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
eloc 38
c 0
b 0
f 0
nc 2808
nop 0
dl 0
loc 66
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
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\PageExport;
16
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\QuizExport;
17
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\ResourceExport;
18
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities\UrlExport;
19
use Exception;
20
use RecursiveDirectoryIterator;
21
use RecursiveIteratorIterator;
22
use ZipArchive;
23
24
use const PATHINFO_EXTENSION;
25
use const PHP_EOL;
26
27
/**
28
 * Class MoodleExport.
29
 * Handles the export of a Moodle course in .mbz format.
30
 */
31
class MoodleExport
32
{
33
    /**
34
     * @var object
35
     */
36
    private $course;
37
38
    /**
39
     * @var array<string,mixed>
40
     */
41
    private static $adminUserData = [];
42
43
    /**
44
     * @var bool selection flag (true when exporting only selected items)
45
     */
46
    private bool $selectionMode = false;
47
48
    /**
49
     * Constructor to initialize the course object.
50
     *
51
     * @param object $course        Filtered legacy course (may be full or selected-only)
52
     * @param bool   $selectionMode When true, do NOT re-hydrate from complete snapshot
53
     */
54
    public function __construct(object $course, bool $selectionMode = false)
55
    {
56
        // Keep the provided (possibly filtered) course as-is.
57
        $this->course = $course;
58
        $this->selectionMode = $selectionMode;
59
60
        // Only auto-fill missing dependencies when doing a full export.
61
        // In selection mode we must not re-inject extra content ("full backup" effect).
62
        if (!$this->selectionMode) {
63
            $cb = new CourseBuilder('complete');
64
            $complete = $cb->build(0, (string) ($course->code ?? ''));
65
66
            // Fill missing resources from learnpath (full export only)
67
            $this->fillResourcesFromLearnpath($complete);
68
69
            // Fill missing quiz questions (full export only)
70
            $this->fillQuestionsFromQuiz($complete);
71
        }
72
    }
73
74
    /**
75
     * Export the Moodle course in .mbz format.
76
     *
77
     * @return string Path to the created .mbz file
78
     */
79
    public function export(string $courseId, string $exportDir, int $version)
80
    {
81
        @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

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

536
        /** @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...
537
538
        $activities = [];
539
        $glossaryAdded = false;
540
541
        // Build a "documents" bucket (root-level files/folders)
542
        $docBucket = [];
543
        if (\defined('RESOURCE_DOCUMENT') && isset($this->course->resources[RESOURCE_DOCUMENT]) && \is_array($this->course->resources[RESOURCE_DOCUMENT])) {
544
            $docBucket = $this->course->resources[RESOURCE_DOCUMENT];
545
        } elseif (isset($this->course->resources['document']) && \is_array($this->course->resources['document'])) {
546
            $docBucket = $this->course->resources['document'];
547
        }
548
        @error_log('[MoodleExport::getActivities] docBucket='.count($docBucket));
549
550
        // Add a visible "Documents" folder activity if we actually have documents
551
        if (!empty($docBucket)) {
552
            $activities[] = [
553
                'id'        => ActivityExport::DOCS_MODULE_ID,
554
                'sectionid' => 0,
555
                'modulename'=> 'folder',
556
                'moduleid'  => ActivityExport::DOCS_MODULE_ID,
557
                'title'     => 'Documents',
558
            ];
559
            @error_log('[MoodleExport::getActivities] Added visible folder activity "Documents" (moduleid=' . ActivityExport::DOCS_MODULE_ID . ').');
560
        }
561
562
        $htmlPageIds = [];
563
564
        foreach ($this->course->resources as $resourceType => $resources) {
565
            if (!\is_array($resources) || empty($resources)) {
566
                continue;
567
            }
568
569
            foreach ($resources as $resource) {
570
                $exportClass = null;
571
                $moduleName = '';
572
                $title = '';
573
                $id = 0;
574
575
                // Quiz
576
                if (RESOURCE_QUIZ === $resourceType && ($resource->obj->iid ?? 0) > 0) {
577
                    $exportClass = QuizExport::class;
578
                    $moduleName = 'quiz';
579
                    $id = (int) $resource->obj->iid;
580
                    $title = (string) $resource->obj->title;
581
                }
582
583
                // URL
584
                if (RESOURCE_LINK === $resourceType && ($resource->source_id ?? 0) > 0) {
585
                    $exportClass = UrlExport::class;
586
                    $moduleName = 'url';
587
                    $id = (int) $resource->source_id;
588
                    $title = (string) ($resource->title ?? '');
589
                }
590
                // Glossary (only once)
591
                elseif (RESOURCE_GLOSSARY === $resourceType && ($resource->glossary_id ?? 0) > 0 && !$glossaryAdded) {
592
                    $exportClass = GlossaryExport::class;
593
                    $moduleName = 'glossary';
594
                    $id = 1;
595
                    $title = get_lang('Glossary');
596
                    $glossaryAdded = true;
597
                }
598
                // Forum
599
                elseif (RESOURCE_FORUM === $resourceType && ($resource->source_id ?? 0) > 0) {
600
                    $exportClass = ForumExport::class;
601
                    $moduleName = 'forum';
602
                    $id = (int) ($resource->obj->iid ?? 0);
603
                    $title = (string) ($resource->obj->forum_title ?? '');
604
                }
605
                // Documents (as Page or Resource)
606
                elseif (RESOURCE_DOCUMENT === $resourceType && ($resource->source_id ?? 0) > 0) {
607
                    $resPath = (string) ($resource->path ?? '');
608
                    $resTitle = (string) ($resource->title ?? '');
609
                    $fileType = (string) ($resource->file_type ?? '');
610
611
                    $isRoot = ('' !== $resPath && 1 === substr_count($resPath, '/'));
612
                    $ext = '' !== $resPath ? pathinfo($resPath, PATHINFO_EXTENSION) : '';
613
614
                    // Root HTML -> export as "page"
615
                    if ('html' === $ext && $isRoot) {
616
                        $exportClass = PageExport::class;
617
                        $moduleName = 'page';
618
                        $id = (int) $resource->source_id;
619
                        $title = $resTitle;
620
                        $htmlPageIds[] = $id;
621
                    }
622
623
                    // Regular file -> export as "resource" (avoid colliding with pages)
624
                    if ('file' === $fileType && !\in_array($resource->source_id, $htmlPageIds, true)) {
625
                        $resourceExport = new ResourceExport($this->course);
626
                        if ($resourceExport->getSectionIdForActivity((int) $resource->source_id, $resourceType) > 0) {
627
                            if ($isRoot) {
628
                                $exportClass = ResourceExport::class;
629
                                $moduleName = 'resource';
630
                                $id = (int) $resource->source_id;
631
                                $title = '' !== $resTitle ? $resTitle : (basename($resPath) ?: ('File '.$id));
632
                            }
633
                        }
634
                    }
635
                }
636
                // *** Tool Intro -> treat "course_homepage" as a Page activity (id=0) ***
637
                elseif (RESOURCE_TOOL_INTRO === $resourceType) {
638
                    // IMPORTANT: do not check source_id; the real key is obj->id
639
                    $objId = (string) ($resource->obj->id ?? '');
640
                    if ($objId === 'course_homepage') {
641
                        $exportClass = PageExport::class;
642
                        $moduleName = 'page';
643
                        // Keep activity id = 0 → PageExport::getData(0, ...) reads the intro HTML
644
                        $id = 0;
645
                        $title = get_lang('Introduction');
646
                    }
647
                }
648
                // Assignments
649
                elseif (RESOURCE_WORK === $resourceType && ($resource->source_id ?? 0) > 0) {
650
                    $exportClass = AssignExport::class;
651
                    $moduleName = 'assign';
652
                    $id = (int) $resource->source_id;
653
                    $title = (string) ($resource->params['title'] ?? '');
654
                }
655
                // Surveys -> Feedback
656
                elseif (RESOURCE_SURVEY === $resourceType && ($resource->source_id ?? 0) > 0) {
657
                    $exportClass = FeedbackExport::class;
658
                    $moduleName = 'feedback';
659
                    $id = (int) $resource->source_id;
660
                    $title = (string) ($resource->params['title'] ?? '');
661
                }
662
663
                // Emit activity if resolved
664
                if ($exportClass && $moduleName) {
665
                    /** @var object $exportInstance */
666
                    $exportInstance = new $exportClass($this->course);
667
                    $sectionId = $exportInstance->getSectionIdForActivity($id, $resourceType);
668
                    $activities[] = [
669
                        'id' => $id,
670
                        'sectionid' => $sectionId,
671
                        'modulename' => $moduleName,
672
                        'moduleid' => $id,
673
                        'title' => $title,
674
                    ];
675
                    @error_log('[MoodleExport::getActivities] ADD modulename='.$moduleName.' moduleid='.$id.' sectionid='.$sectionId.' title="'.str_replace(["\n","\r"],' ',$title).'"');
676
                }
677
            }
678
        }
679
680
        @error_log('[MoodleExport::getActivities] Done. total='.count($activities));
681
        return $activities;
682
    }
683
684
    /**
685
     * Collect Moodle URL activities from legacy "link" bucket.
686
     *
687
     * It is defensive against different wrappers:
688
     * - Accepts link objects as $wrap->obj or directly as $wrap.
689
     * - Resolves title from title|name|url (last-resort).
690
     * - Maps category_id to a section name (category title) if available.
691
     *
692
     * @return UrlExport[]
693
     */
694
    private function buildUrlActivities(): array
695
    {
696
        $res = \is_array($this->course->resources ?? null) ? $this->course->resources : [];
697
698
        // Buckets (defensive: allow legacy casings)
699
        $links = $res['link'] ?? $res['Link'] ?? [];
700
        $cats  = $res['link_category'] ?? $res['Link_Category'] ?? [];
701
702
        // Map category_id → label for section naming
703
        $catLabel = [];
704
        foreach ($cats as $cid => $cwrap) {
705
            if (!\is_object($cwrap)) {
706
                continue;
707
            }
708
            $c = (isset($cwrap->obj) && \is_object($cwrap->obj)) ? $cwrap->obj : $cwrap;
709
            $label = '';
710
            foreach (['title', 'name'] as $k) {
711
                if (!empty($c->{$k}) && \is_string($c->{$k})) {
712
                    $label = trim((string) $c->{$k});
713
                    break;
714
                }
715
            }
716
            $catLabel[(int) $cid] = $label !== '' ? $label : ('Category #'.(int) $cid);
717
        }
718
719
        $out = [];
720
        foreach ($links as $id => $lwrap) {
721
            if (!\is_object($lwrap)) {
722
                continue;
723
            }
724
            $L = (isset($lwrap->obj) && \is_object($lwrap->obj)) ? $lwrap->obj : $lwrap;
725
726
            $url = (string) ($L->url ?? '');
727
            if ($url === '') {
728
                // Skip invalid URL records
729
                continue;
730
            }
731
732
            // Resolve a robust title
733
            $title = '';
734
            foreach (['title', 'name'] as $k) {
735
                if (!empty($L->{$k}) && \is_string($L->{$k})) {
736
                    $title = trim((string) $L->{$k});
737
                    break;
738
                }
739
            }
740
            if ($title === '') {
741
                $title = $url; // last resort: use the URL itself
742
            }
743
744
            $target = (string) ($L->target ?? '');
745
            $intro  = (string) ($L->description ?? '');
746
            $cid    = (int) ($L->category_id ?? 0);
747
748
            $sectionName = $catLabel[$cid] ?? null;
749
750
            // UrlExport ctor: (string $title, string $url, ?string $section = null, ?string $introHtml = null, ?string $target = null)
751
            $urlAct = new UrlExport($title, $url, $sectionName ?: null, $intro ?: null, $target ?: null);
752
            if (method_exists($urlAct, 'setLegacyId')) {
753
                $urlAct->setLegacyId((int) $id);
754
            }
755
756
            $out[] = $urlAct;
757
        }
758
759
        return $out;
760
    }
761
762
    /**
763
     * Enqueue all URL activities into the export pipeline.
764
     * Will try queueActivity(), then addActivity(), then $this->activities[].
765
     */
766
    private function enqueueUrlActivities(): void
767
    {
768
        $urls = $this->buildUrlActivities();
769
770
        if (empty($urls)) {
771
            @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

771
            /** @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...
772
            return;
773
        }
774
775
        if (method_exists($this, 'queueActivity')) {
776
            foreach ($urls as $act) {
777
                $this->queueActivity($act);
778
            }
779
            @error_log('[MoodleExport] URL activities enqueued via queueActivity(): '.count($urls));
780
            return;
781
        }
782
783
        if (method_exists($this, 'addActivity')) {
784
            foreach ($urls as $act) {
785
                $this->addActivity($act);
786
            }
787
            @error_log('[MoodleExport] URL activities appended via addActivity(): '.count($urls));
788
            return;
789
        }
790
791
        if (property_exists($this, 'activities') && \is_array($this->activities)) {
792
            array_push($this->activities, ...$urls);
793
            @error_log('[MoodleExport] URL activities appended to $this->activities: '.count($urls));
794
            return;
795
        }
796
797
        @error_log('[MoodleExport][WARN] Could not enqueue URL activities (no compatible method found)');
798
    }
799
800
    private function exportSections(string $exportDir): void
801
    {
802
        $sections = $this->getSections();
803
        foreach ($sections as $section) {
804
            $sectionExport = new SectionExport($this->course);
805
            $sectionExport->exportSection($section['id'], $exportDir);
806
        }
807
    }
808
809
    private function createMbzFile(string $sourceDir): string
810
    {
811
        $zip = new ZipArchive();
812
        $zipFile = $sourceDir.'.mbz';
813
814
        if (true !== $zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE)) {
815
            throw new Exception(get_lang('ErrorCreatingZip'));
816
        }
817
818
        $files = new RecursiveIteratorIterator(
819
            new RecursiveDirectoryIterator($sourceDir),
820
            RecursiveIteratorIterator::LEAVES_ONLY
821
        );
822
823
        foreach ($files as $file) {
824
            if (!$file->isDir()) {
825
                $filePath = $file->getRealPath();
826
                $relativePath = substr($filePath, \strlen($sourceDir) + 1);
827
828
                if (!$zip->addFile($filePath, $relativePath)) {
829
                    throw new Exception(get_lang('ErrorAddingFileToZip').": $relativePath");
830
                }
831
            }
832
        }
833
834
        if (!$zip->close()) {
835
            throw new Exception(get_lang('ErrorClosingZip'));
836
        }
837
838
        return $zipFile;
839
    }
840
841
    private function cleanupTempDir(string $dir): void
842
    {
843
        $this->recursiveDelete($dir);
844
    }
845
846
    private function recursiveDelete(string $dir): void
847
    {
848
        $files = array_diff(scandir($dir), ['.', '..']);
849
        foreach ($files as $file) {
850
            $path = "$dir/$file";
851
            is_dir($path) ? $this->recursiveDelete($path) : unlink($path);
852
        }
853
        rmdir($dir);
854
    }
855
856
    private function exportBadgesXml(string $exportDir): void
857
    {
858
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
859
        $xmlContent .= '<badges>'.PHP_EOL;
860
        $xmlContent .= '</badges>';
861
        file_put_contents($exportDir.'/badges.xml', $xmlContent);
862
    }
863
864
    private function exportCompletionXml(string $exportDir): void
865
    {
866
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
867
        $xmlContent .= '<completions>'.PHP_EOL;
868
        $xmlContent .= '</completions>';
869
        file_put_contents($exportDir.'/completion.xml', $xmlContent);
870
    }
871
872
    private function exportGradebookXml(string $exportDir): void
873
    {
874
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
875
        $xmlContent .= '<gradebook>'.PHP_EOL;
876
        $xmlContent .= '</gradebook>';
877
        file_put_contents($exportDir.'/gradebook.xml', $xmlContent);
878
    }
879
880
    private function exportGradeHistoryXml(string $exportDir): void
881
    {
882
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
883
        $xmlContent .= '<grade_history>'.PHP_EOL;
884
        $xmlContent .= '</grade_history>';
885
        file_put_contents($exportDir.'/grade_history.xml', $xmlContent);
886
    }
887
888
    private function exportGroupsXml(string $exportDir): void
889
    {
890
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
891
        $xmlContent .= '<groups>'.PHP_EOL;
892
        $xmlContent .= '</groups>';
893
        file_put_contents($exportDir.'/groups.xml', $xmlContent);
894
    }
895
896
    private function exportOutcomesXml(string $exportDir): void
897
    {
898
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
899
        $xmlContent .= '<outcomes>'.PHP_EOL;
900
        $xmlContent .= '</outcomes>';
901
        file_put_contents($exportDir.'/outcomes.xml', $xmlContent);
902
    }
903
904
    private function exportRolesXml(string $exportDir): void
905
    {
906
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
907
        $xmlContent .= '<roles_definition>'.PHP_EOL;
908
        $xmlContent .= '  <role id="5">'.PHP_EOL;
909
        $xmlContent .= '    <name></name>'.PHP_EOL;
910
        $xmlContent .= '    <shortname>student</shortname>'.PHP_EOL;
911
        $xmlContent .= '    <nameincourse>$@NULL@$</nameincourse>'.PHP_EOL;
912
        $xmlContent .= '    <description></description>'.PHP_EOL;
913
        $xmlContent .= '    <sortorder>5</sortorder>'.PHP_EOL;
914
        $xmlContent .= '    <archetype>student</archetype>'.PHP_EOL;
915
        $xmlContent .= '  </role>'.PHP_EOL;
916
        $xmlContent .= '</roles_definition>'.PHP_EOL;
917
918
        file_put_contents($exportDir.'/roles.xml', $xmlContent);
919
    }
920
921
    private function exportScalesXml(string $exportDir): void
922
    {
923
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
924
        $xmlContent .= '<scales>'.PHP_EOL;
925
        $xmlContent .= '</scales>';
926
        file_put_contents($exportDir.'/scales.xml', $xmlContent);
927
    }
928
929
    private function exportUsersXml(string $exportDir): void
930
    {
931
        $adminData = self::getAdminUserData();
932
933
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
934
        $xmlContent .= '<users>'.PHP_EOL;
935
        $xmlContent .= '  <user id="'.$adminData['id'].'" contextid="'.$adminData['contextid'].'">'.PHP_EOL;
936
        $xmlContent .= '    <username>'.$adminData['username'].'</username>'.PHP_EOL;
937
        $xmlContent .= '    <idnumber>'.$adminData['idnumber'].'</idnumber>'.PHP_EOL;
938
        $xmlContent .= '    <email>'.$adminData['email'].'</email>'.PHP_EOL;
939
        $xmlContent .= '    <phone1>'.$adminData['phone1'].'</phone1>'.PHP_EOL;
940
        $xmlContent .= '    <phone2>'.$adminData['phone2'].'</phone2>'.PHP_EOL;
941
        $xmlContent .= '    <institution>'.$adminData['institution'].'</institution>'.PHP_EOL;
942
        $xmlContent .= '    <department>'.$adminData['department'].'</department>'.PHP_EOL;
943
        $xmlContent .= '    <address>'.$adminData['address'].'</address>'.PHP_EOL;
944
        $xmlContent .= '    <city>'.$adminData['city'].'</city>'.PHP_EOL;
945
        $xmlContent .= '    <country>'.$adminData['country'].'</country>'.PHP_EOL;
946
        $xmlContent .= '    <lastip>'.$adminData['lastip'].'</lastip>'.PHP_EOL;
947
        $xmlContent .= '    <picture>'.$adminData['picture'].'</picture>'.PHP_EOL;
948
        $xmlContent .= '    <description>'.$adminData['description'].'</description>'.PHP_EOL;
949
        $xmlContent .= '    <descriptionformat>'.$adminData['descriptionformat'].'</descriptionformat>'.PHP_EOL;
950
        $xmlContent .= '    <imagealt>'.$adminData['imagealt'].'</imagealt>'.PHP_EOL;
951
        $xmlContent .= '    <auth>'.$adminData['auth'].'</auth>'.PHP_EOL;
952
        $xmlContent .= '    <firstname>'.$adminData['firstname'].'</firstname>'.PHP_EOL;
953
        $xmlContent .= '    <lastname>'.$adminData['lastname'].'</lastname>'.PHP_EOL;
954
        $xmlContent .= '    <confirmed>'.$adminData['confirmed'].'</confirmed>'.PHP_EOL;
955
        $xmlContent .= '    <policyagreed>'.$adminData['policyagreed'].'</policyagreed>'.PHP_EOL;
956
        $xmlContent .= '    <deleted>'.$adminData['deleted'].'</deleted>'.PHP_EOL;
957
        $xmlContent .= '    <lang>'.$adminData['lang'].'</lang>'.PHP_EOL;
958
        $xmlContent .= '    <theme>'.$adminData['theme'].'</theme>'.PHP_EOL;
959
        $xmlContent .= '    <timezone>'.$adminData['timezone'].'</timezone>'.PHP_EOL;
960
        $xmlContent .= '    <firstaccess>'.$adminData['firstaccess'].'</firstaccess>'.PHP_EOL;
961
        $xmlContent .= '    <lastaccess>'.$adminData['lastaccess'].'</lastaccess>'.PHP_EOL;
962
        $xmlContent .= '    <lastlogin>'.$adminData['lastlogin'].'</lastlogin>'.PHP_EOL;
963
        $xmlContent .= '    <currentlogin>'.$adminData['currentlogin'].'</currentlogin>'.PHP_EOL;
964
        $xmlContent .= '    <mailformat>'.$adminData['mailformat'].'</mailformat>'.PHP_EOL;
965
        $xmlContent .= '    <maildigest>'.$adminData['maildigest'].'</maildigest>'.PHP_EOL;
966
        $xmlContent .= '    <maildisplay>'.$adminData['maildisplay'].'</maildisplay>'.PHP_EOL;
967
        $xmlContent .= '    <autosubscribe>'.$adminData['autosubscribe'].'</autosubscribe>'.PHP_EOL;
968
        $xmlContent .= '    <trackforums>'.$adminData['trackforums'].'</trackforums>'.PHP_EOL;
969
        $xmlContent .= '    <timecreated>'.$adminData['timecreated'].'</timecreated>'.PHP_EOL;
970
        $xmlContent .= '    <timemodified>'.$adminData['timemodified'].'</timemodified>'.PHP_EOL;
971
        $xmlContent .= '    <trustbitmask>'.$adminData['trustbitmask'].'</trustbitmask>'.PHP_EOL;
972
973
        if (isset($adminData['preferences']) && \is_array($adminData['preferences'])) {
974
            $xmlContent .= '    <preferences>'.PHP_EOL;
975
            foreach ($adminData['preferences'] as $preference) {
976
                $xmlContent .= '      <preference>'.PHP_EOL;
977
                $xmlContent .= '        <name>'.htmlspecialchars((string) $preference['name']).'</name>'.PHP_EOL;
978
                $xmlContent .= '        <value>'.htmlspecialchars((string) $preference['value']).'</value>'.PHP_EOL;
979
                $xmlContent .= '      </preference>'.PHP_EOL;
980
            }
981
            $xmlContent .= '    </preferences>'.PHP_EOL;
982
        } else {
983
            $xmlContent .= '    <preferences></preferences>'.PHP_EOL;
984
        }
985
986
        $xmlContent .= '    <roles>'.PHP_EOL;
987
        $xmlContent .= '      <role_overrides></role_overrides>'.PHP_EOL;
988
        $xmlContent .= '      <role_assignments></role_assignments>'.PHP_EOL;
989
        $xmlContent .= '    </roles>'.PHP_EOL;
990
991
        $xmlContent .= '  </user>'.PHP_EOL;
992
        $xmlContent .= '</users>';
993
994
        file_put_contents($exportDir.'/users.xml', $xmlContent);
995
    }
996
997
    private function exportBackupSettings(array $sections, array $activities): array
998
    {
999
        $settings = [
1000
            ['level' => 'root', 'name' => 'filename', 'value' => 'backup-moodle-course-'.time().'.mbz'],
1001
            ['level' => 'root', 'name' => 'imscc11', 'value' => '0'],
1002
            ['level' => 'root', 'name' => 'users', 'value' => '1'],
1003
            ['level' => 'root', 'name' => 'anonymize', 'value' => '0'],
1004
            ['level' => 'root', 'name' => 'role_assignments', 'value' => '1'],
1005
            ['level' => 'root', 'name' => 'activities', 'value' => '1'],
1006
            ['level' => 'root', 'name' => 'blocks', 'value' => '1'],
1007
            ['level' => 'root', 'name' => 'files', 'value' => '1'],
1008
            ['level' => 'root', 'name' => 'filters', 'value' => '1'],
1009
            ['level' => 'root', 'name' => 'comments', 'value' => '1'],
1010
            ['level' => 'root', 'name' => 'badges', 'value' => '1'],
1011
            ['level' => 'root', 'name' => 'calendarevents', 'value' => '1'],
1012
            ['level' => 'root', 'name' => 'userscompletion', 'value' => '1'],
1013
            ['level' => 'root', 'name' => 'logs', 'value' => '0'],
1014
            ['level' => 'root', 'name' => 'grade_histories', 'value' => '0'],
1015
            ['level' => 'root', 'name' => 'questionbank', 'value' => '1'],
1016
            ['level' => 'root', 'name' => 'groups', 'value' => '1'],
1017
            ['level' => 'root', 'name' => 'competencies', 'value' => '0'],
1018
            ['level' => 'root', 'name' => 'customfield', 'value' => '1'],
1019
            ['level' => 'root', 'name' => 'contentbankcontent', 'value' => '1'],
1020
            ['level' => 'root', 'name' => 'legacyfiles', 'value' => '1'],
1021
        ];
1022
1023
        foreach ($sections as $section) {
1024
            $settings[] = [
1025
                'level' => 'section',
1026
                'section' => 'section_'.$section['id'],
1027
                'name' => 'section_'.$section['id'].'_included',
1028
                'value' => '1',
1029
            ];
1030
            $settings[] = [
1031
                'level' => 'section',
1032
                'section' => 'section_'.$section['id'],
1033
                'name' => 'section_'.$section['id'].'_userinfo',
1034
                'value' => '1',
1035
            ];
1036
        }
1037
1038
        foreach ($activities as $activity) {
1039
            $settings[] = [
1040
                'level' => 'activity',
1041
                'activity' => $activity['modulename'].'_'.$activity['moduleid'],
1042
                'name' => $activity['modulename'].'_'.$activity['moduleid'].'_included',
1043
                'value' => '1',
1044
            ];
1045
            $settings[] = [
1046
                'level' => 'activity',
1047
                'activity' => $activity['modulename'].'_'.$activity['moduleid'],
1048
                'name' => $activity['modulename'].'_'.$activity['moduleid'].'_userinfo',
1049
                'value' => '1',
1050
            ];
1051
        }
1052
1053
        return $settings;
1054
    }
1055
}
1056