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