1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* For licensing terms, see /license.txt */ |
4
|
|
|
|
5
|
|
|
namespace moodleexport; |
6
|
|
|
|
7
|
|
|
use Exception; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* Class SectionExport. |
11
|
|
|
* Handles the export of course sections and their activities. |
12
|
|
|
* |
13
|
|
|
* @package moodleexport |
14
|
|
|
*/ |
15
|
|
|
class SectionExport |
16
|
|
|
{ |
17
|
|
|
private $course; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Constructor to initialize the course object. |
21
|
|
|
* |
22
|
|
|
* @param object $course The course object to be exported. |
23
|
|
|
*/ |
24
|
|
|
public function __construct($course) |
25
|
|
|
{ |
26
|
|
|
$this->course = $course; |
27
|
|
|
} |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Export a section and its activities to the specified directory. |
31
|
|
|
*/ |
32
|
|
|
public function exportSection(int $sectionId, string $exportDir): void |
33
|
|
|
{ |
34
|
|
|
$sectionDir = $exportDir."/sections/section_{$sectionId}"; |
35
|
|
|
|
36
|
|
|
if (!is_dir($sectionDir)) { |
37
|
|
|
mkdir($sectionDir, api_get_permissions_for_new_directories(), true); |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
if ($sectionId > 0) { |
41
|
|
|
$learnpath = $this->getLearnpathById($sectionId); |
42
|
|
|
if ($learnpath === null) { |
43
|
|
|
throw new Exception("Learnpath with ID $sectionId not found."); |
44
|
|
|
} |
45
|
|
|
$sectionData = $this->getSectionData($learnpath); |
46
|
|
|
} else { |
47
|
|
|
$sectionData = [ |
48
|
|
|
'id' => 0, |
49
|
|
|
'number' => 0, |
50
|
|
|
'name' => get_lang('General'), |
51
|
|
|
'summary' => get_lang('GeneralResourcesCourse'), |
52
|
|
|
'sequence' => 0, |
53
|
|
|
'visible' => 1, |
54
|
|
|
'timemodified' => time(), |
55
|
|
|
'activities' => $this->getActivitiesForGeneral(), |
56
|
|
|
]; |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
$this->createSectionXml($sectionData, $sectionDir); |
60
|
|
|
$this->createInforefXml($sectionData, $sectionDir); |
61
|
|
|
$this->exportActivities($sectionData['activities'], $exportDir, $sectionId); |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Get all general items not linked to any lesson (learnpath). |
66
|
|
|
*/ |
67
|
|
|
public function getGeneralItems(): array |
68
|
|
|
{ |
69
|
|
|
$generalItems = []; |
70
|
|
|
|
71
|
|
|
// List of resource types and their corresponding ID keys |
72
|
|
|
$resourceTypes = [ |
73
|
|
|
RESOURCE_DOCUMENT => 'source_id', |
74
|
|
|
RESOURCE_QUIZ => 'source_id', |
75
|
|
|
RESOURCE_GLOSSARY => 'glossary_id', |
76
|
|
|
RESOURCE_LINK => 'source_id', |
77
|
|
|
RESOURCE_WORK => 'source_id', |
78
|
|
|
RESOURCE_FORUM => 'source_id', |
79
|
|
|
RESOURCE_SURVEY => 'source_id', |
80
|
|
|
RESOURCE_TOOL_INTRO => 'source_id', |
81
|
|
|
]; |
82
|
|
|
|
83
|
|
|
foreach ($resourceTypes as $resourceType => $idKey) { |
84
|
|
|
if (!empty($this->course->resources[$resourceType])) { |
85
|
|
|
foreach ($this->course->resources[$resourceType] as $id => $resource) { |
86
|
|
|
if (!$this->isItemInLearnpath($resource, $resourceType)) { |
87
|
|
|
$title = $resourceType === RESOURCE_WORK |
88
|
|
|
? ($resource->params['title'] ?? '') |
89
|
|
|
: ($resource->title ?? $resource->name); |
90
|
|
|
$generalItems[] = [ |
91
|
|
|
'id' => $resource->$idKey, |
92
|
|
|
'item_type' => $resourceType, |
93
|
|
|
'path' => $id, |
94
|
|
|
'title' => $title, |
95
|
|
|
]; |
96
|
|
|
} |
97
|
|
|
} |
98
|
|
|
} |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
return $generalItems; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Get the activities for the general section. |
106
|
|
|
*/ |
107
|
|
|
public function getActivitiesForGeneral(): array |
108
|
|
|
{ |
109
|
|
|
$generalLearnpath = (object) [ |
110
|
|
|
'items' => $this->getGeneralItems(), |
111
|
|
|
'source_id' => 0, |
112
|
|
|
]; |
113
|
|
|
|
114
|
|
|
$activities = $this->getActivitiesForSection($generalLearnpath, true); |
115
|
|
|
|
116
|
|
|
if (!in_array('folder', array_column($activities, 'modulename'))) { |
117
|
|
|
$activities[] = [ |
118
|
|
|
'id' => 0, |
119
|
|
|
'moduleid' => 0, |
120
|
|
|
'modulename' => 'folder', |
121
|
|
|
'name' => 'Documents', |
122
|
|
|
'sectionid' => 0, |
123
|
|
|
]; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
return $activities; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Get the learnpath object by its ID. |
131
|
|
|
*/ |
132
|
|
|
public function getLearnpathById(int $sectionId): ?object |
133
|
|
|
{ |
134
|
|
|
foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { |
135
|
|
|
if ($learnpath->source_id == $sectionId) { |
136
|
|
|
return $learnpath; |
137
|
|
|
} |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
return null; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Get section data for a learnpath. |
145
|
|
|
*/ |
146
|
|
|
public function getSectionData(object $learnpath): array |
147
|
|
|
{ |
148
|
|
|
return [ |
149
|
|
|
'id' => $learnpath->source_id, |
150
|
|
|
'number' => $learnpath->display_order, |
151
|
|
|
'name' => $learnpath->name, |
152
|
|
|
'summary' => $learnpath->description, |
153
|
|
|
'sequence' => $learnpath->source_id, |
154
|
|
|
'visible' => $learnpath->visibility, |
155
|
|
|
'timemodified' => strtotime($learnpath->modified_on), |
156
|
|
|
'activities' => $this->getActivitiesForSection($learnpath), |
157
|
|
|
]; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Get the activities for a specific section. |
162
|
|
|
*/ |
163
|
|
|
public function getActivitiesForSection(object $learnpath, bool $isGeneral = false): array |
164
|
|
|
{ |
165
|
|
|
$activities = []; |
166
|
|
|
$sectionId = $isGeneral ? 0 : $learnpath->source_id; |
167
|
|
|
|
168
|
|
|
foreach ($learnpath->items as $item) { |
169
|
|
|
$this->addActivityToList($item, $sectionId, $activities); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
return $activities; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* Export the activities of a section. |
177
|
|
|
*/ |
178
|
|
|
private function exportActivities(array $activities, string $exportDir, int $sectionId): void |
179
|
|
|
{ |
180
|
|
|
$exportClasses = [ |
181
|
|
|
'quiz' => QuizExport::class, |
182
|
|
|
'glossary' => GlossaryExport::class, |
183
|
|
|
'url' => UrlExport::class, |
184
|
|
|
'assign' => AssignExport::class, |
185
|
|
|
'forum' => ForumExport::class, |
186
|
|
|
'page' => PageExport::class, |
187
|
|
|
'resource' => ResourceExport::class, |
188
|
|
|
'folder' => FolderExport::class, |
189
|
|
|
'feedback' => FeedbackExport::class, |
190
|
|
|
]; |
191
|
|
|
|
192
|
|
|
foreach ($activities as $activity) { |
193
|
|
|
$moduleName = $activity['modulename']; |
194
|
|
|
if (isset($exportClasses[$moduleName])) { |
195
|
|
|
$exportClass = new $exportClasses[$moduleName]($this->course); |
196
|
|
|
$exportClass->export($activity['id'], $exportDir, $activity['moduleid'], $sectionId); |
197
|
|
|
} else { |
198
|
|
|
throw new \Exception("Export for module '$moduleName' is not supported."); |
199
|
|
|
} |
200
|
|
|
} |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Check if an item is associated with any learnpath. |
205
|
|
|
*/ |
206
|
|
|
private function isItemInLearnpath(object $item, string $type): bool |
207
|
|
|
{ |
208
|
|
|
if (!empty($this->course->resources[RESOURCE_LEARNPATH])) { |
209
|
|
|
foreach ($this->course->resources[RESOURCE_LEARNPATH] as $learnpath) { |
210
|
|
|
if (!empty($learnpath->items)) { |
211
|
|
|
foreach ($learnpath->items as $learnpathItem) { |
212
|
|
|
if ($learnpathItem['item_type'] === $type && $learnpathItem['path'] == $item->source_id) { |
213
|
|
|
return true; |
214
|
|
|
} |
215
|
|
|
} |
216
|
|
|
} |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
return false; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* Add an activity to the activities list. |
225
|
|
|
*/ |
226
|
|
|
private function addActivityToList(array $item, int $sectionId, array &$activities): void |
227
|
|
|
{ |
228
|
|
|
static $documentsFolderAdded = false; |
229
|
|
|
if (!$documentsFolderAdded && $sectionId === 0) { |
230
|
|
|
$activities[] = [ |
231
|
|
|
'id' => 0, |
232
|
|
|
'moduleid' => 0, |
233
|
|
|
'type' => 'folder', |
234
|
|
|
'modulename' => 'folder', |
235
|
|
|
'name' => 'Documents', |
236
|
|
|
]; |
237
|
|
|
$documentsFolderAdded = true; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
$activityData = null; |
241
|
|
|
$activityClassMap = [ |
242
|
|
|
'quiz' => QuizExport::class, |
243
|
|
|
'glossary' => GlossaryExport::class, |
244
|
|
|
'url' => UrlExport::class, |
245
|
|
|
'assign' => AssignExport::class, |
246
|
|
|
'forum' => ForumExport::class, |
247
|
|
|
'page' => PageExport::class, |
248
|
|
|
'resource' => ResourceExport::class, |
249
|
|
|
'feedback' => FeedbackExport::class, |
250
|
|
|
]; |
251
|
|
|
|
252
|
|
|
if ($item['id'] == 'course_homepage') { |
253
|
|
|
$item['item_type'] = 'page'; |
254
|
|
|
$item['path'] = 0; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
$itemType = $item['item_type'] === 'link' ? 'url' : |
258
|
|
|
($item['item_type'] === 'work' ? 'assign' : |
259
|
|
|
($item['item_type'] === 'survey' ? 'feedback' : $item['item_type'])); |
260
|
|
|
|
261
|
|
|
switch ($itemType) { |
262
|
|
|
case 'quiz': |
263
|
|
|
case 'glossary': |
264
|
|
|
case 'assign': |
265
|
|
|
case 'url': |
266
|
|
|
case 'forum': |
267
|
|
|
case 'feedback': |
268
|
|
|
case 'page': |
269
|
|
|
$activityId = $itemType === 'glossary' ? 1 : (int) $item['path']; |
270
|
|
|
$exportClass = $activityClassMap[$itemType]; |
271
|
|
|
$exportInstance = new $exportClass($this->course); |
272
|
|
|
$activityData = $exportInstance->getData($activityId, $sectionId); |
273
|
|
|
break; |
274
|
|
|
|
275
|
|
|
case 'document': |
276
|
|
|
$documentId = (int) $item['path']; |
277
|
|
|
$document = \DocumentManager::get_document_data_by_id($documentId, $this->course->code); |
278
|
|
|
|
279
|
|
|
if ($document) { |
|
|
|
|
280
|
|
|
$isRoot = substr_count($document['path'], '/') === 1; |
281
|
|
|
$documentType = $this->getDocumentType($document['filetype'], $document['path']); |
282
|
|
|
if ($documentType === 'page' && $isRoot) { |
283
|
|
|
$activityClass = $activityClassMap['page']; |
284
|
|
|
$exportInstance = new $activityClass($this->course); |
285
|
|
|
$activityData = $exportInstance->getData($item['path'], $sectionId); |
286
|
|
|
} |
287
|
|
|
elseif ($sectionId > 0 && $documentType && isset($activityClassMap[$documentType])) { |
288
|
|
|
$activityClass = $activityClassMap[$documentType]; |
289
|
|
|
$exportInstance = new $activityClass($this->course); |
290
|
|
|
$activityData = $exportInstance->getData($item['path'], $sectionId); |
291
|
|
|
} |
292
|
|
|
} |
293
|
|
|
break; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
// Add the activity to the list if the data exists |
297
|
|
|
if ($activityData) { |
298
|
|
|
$activities[] = [ |
299
|
|
|
'id' => $activityData['id'], |
300
|
|
|
'moduleid' => $activityData['moduleid'], |
301
|
|
|
'type' => $item['item_type'], |
302
|
|
|
'modulename' => $activityData['modulename'], |
303
|
|
|
'name' => $activityData['name'], |
304
|
|
|
]; |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Determine the document type based on filetype and path. |
310
|
|
|
*/ |
311
|
|
|
private function getDocumentType(string $filetype, string $path): ?string |
312
|
|
|
{ |
313
|
|
|
if ('html' === pathinfo($path, PATHINFO_EXTENSION)) { |
314
|
|
|
return 'page'; |
315
|
|
|
} elseif ('file' === $filetype) { |
316
|
|
|
return 'resource'; |
317
|
|
|
} /*elseif ('folder' === $filetype) { |
318
|
|
|
return 'folder'; |
319
|
|
|
}*/ |
320
|
|
|
|
321
|
|
|
return null; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* Create the section.xml file. |
326
|
|
|
*/ |
327
|
|
|
private function createSectionXml(array $sectionData, string $destinationDir): void |
328
|
|
|
{ |
329
|
|
|
$xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL; |
330
|
|
|
$xmlContent .= '<section id="'.$sectionData['id'].'">'.PHP_EOL; |
331
|
|
|
$xmlContent .= ' <number>'.$sectionData['number'].'</number>'.PHP_EOL; |
332
|
|
|
$xmlContent .= ' <name>'.htmlspecialchars($sectionData['name']).'</name>'.PHP_EOL; |
333
|
|
|
$xmlContent .= ' <summary>'.htmlspecialchars($sectionData['summary']).'</summary>'.PHP_EOL; |
334
|
|
|
$xmlContent .= ' <summaryformat>1</summaryformat>'.PHP_EOL; |
335
|
|
|
$xmlContent .= ' <sequence>'.implode(',', array_column($sectionData['activities'], 'moduleid')).'</sequence>'.PHP_EOL; |
336
|
|
|
$xmlContent .= ' <visible>'.$sectionData['visible'].'</visible>'.PHP_EOL; |
337
|
|
|
$xmlContent .= ' <timemodified>'.$sectionData['timemodified'].'</timemodified>'.PHP_EOL; |
338
|
|
|
$xmlContent .= '</section>'.PHP_EOL; |
339
|
|
|
|
340
|
|
|
$xmlFile = $destinationDir.'/section.xml'; |
341
|
|
|
file_put_contents($xmlFile, $xmlContent); |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* Create the inforef.xml file for the section. |
346
|
|
|
*/ |
347
|
|
|
private function createInforefXml(array $sectionData, string $destinationDir): void |
348
|
|
|
{ |
349
|
|
|
$xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL; |
350
|
|
|
$xmlContent .= '<inforef>'.PHP_EOL; |
351
|
|
|
|
352
|
|
|
foreach ($sectionData['activities'] as $activity) { |
353
|
|
|
$xmlContent .= ' <activity id="'.$activity['id'].'">'.htmlspecialchars($activity['name']).'</activity>'.PHP_EOL; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
$xmlContent .= '</inforef>'.PHP_EOL; |
357
|
|
|
|
358
|
|
|
$xmlFile = $destinationDir.'/inforef.xml'; |
359
|
|
|
file_put_contents($xmlFile, $xmlContent); |
360
|
|
|
} |
361
|
|
|
} |
362
|
|
|
|
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.