Total Complexity | 136 |
Total Lines | 868 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like MoodleExport often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use MoodleExport, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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 |
||
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 |
||
650 | } |
||
651 | } |
||
652 | |||
653 | private function createMbzFile(string $sourceDir): string |
||
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 |
||
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.