|
1
|
|
|
<?php |
|
2
|
|
|
declare(strict_types=1); |
|
3
|
|
|
|
|
4
|
|
|
/* For licensing terms, see /license.txt */ |
|
5
|
|
|
|
|
6
|
|
|
namespace Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities; |
|
7
|
|
|
|
|
8
|
|
|
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Builder\FileExport; |
|
9
|
|
|
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Builder\MoodleExport; |
|
10
|
|
|
use DocumentManager; |
|
11
|
|
|
|
|
12
|
|
|
use const ENT_QUOTES; |
|
13
|
|
|
use const ENT_SUBSTITUTE; |
|
14
|
|
|
use const PHP_EOL; |
|
15
|
|
|
|
|
16
|
|
|
/** |
|
17
|
|
|
* LabelExport — exports legacy Course Descriptions as Moodle "label" activities. |
|
18
|
|
|
* - Writes activities/label_{moduleId}/{label.xml,module.xml,inforef.xml,...} |
|
19
|
|
|
* - Uses ActivityExport helpers for module.xml, inforef.xml, etc. |
|
20
|
|
|
*/ |
|
21
|
|
|
class LabelExport extends ActivityExport |
|
22
|
|
|
{ |
|
23
|
|
|
/** |
|
24
|
|
|
* Export this label activity. |
|
25
|
|
|
* |
|
26
|
|
|
* @param int $activityId source_id of the course_description |
|
27
|
|
|
* @param string $exportDir root temp export directory |
|
28
|
|
|
* @param int $moduleId module id used in directory name (usually = $activityId) |
|
29
|
|
|
* @param int $sectionId resolved section (LP) or 0 for General |
|
30
|
|
|
*/ |
|
31
|
|
|
public function export(int $activityId, string $exportDir, int $moduleId, int $sectionId): void |
|
32
|
|
|
{ |
|
33
|
|
|
// Ensure activity folder |
|
34
|
|
|
$labelDir = $this->prepareActivityDirectory($exportDir, 'label', $moduleId); |
|
35
|
|
|
|
|
36
|
|
|
// Resolve payload |
|
37
|
|
|
$data = $this->getData($activityId, $sectionId); |
|
38
|
|
|
if (null === $data) { |
|
39
|
|
|
// Nothing to export |
|
40
|
|
|
return; |
|
41
|
|
|
} |
|
42
|
|
|
|
|
43
|
|
|
// Write primary XMLs |
|
44
|
|
|
$this->createLabelXml($data, $labelDir); |
|
45
|
|
|
$this->createModuleXml($data, $labelDir); |
|
46
|
|
|
$this->createInforefXml($data, $labelDir); |
|
47
|
|
|
|
|
48
|
|
|
// Optional, but keeps structure consistent with other exporters |
|
49
|
|
|
$this->createFiltersXml($data, $labelDir); |
|
50
|
|
|
$this->createGradesXml($data, $labelDir); |
|
51
|
|
|
$this->createGradeHistoryXml($data, $labelDir); |
|
52
|
|
|
$this->createCompletionXml($data, $labelDir); |
|
53
|
|
|
$this->createCommentsXml($data, $labelDir); |
|
54
|
|
|
$this->createCompetenciesXml($data, $labelDir); |
|
55
|
|
|
$this->createRolesXml($data, $labelDir); |
|
56
|
|
|
$this->createCalendarXml($data, $labelDir); |
|
57
|
|
|
|
|
58
|
|
|
} |
|
59
|
|
|
|
|
60
|
|
|
/** |
|
61
|
|
|
* Build label payload from legacy "course_description" bucket. |
|
62
|
|
|
*/ |
|
63
|
|
|
public function getData(int $labelId, int $sectionId): ?array |
|
64
|
|
|
{ |
|
65
|
|
|
// Accept both constant and plain string, defensively |
|
66
|
|
|
$bag = |
|
67
|
|
|
$this->course->resources[\defined('RESOURCE_COURSEDESCRIPTION') ? RESOURCE_COURSEDESCRIPTION : 'course_description'] |
|
68
|
|
|
?? $this->course->resources['course_description'] |
|
69
|
|
|
?? []; |
|
70
|
|
|
|
|
71
|
|
|
if (empty($bag) || !\is_array($bag)) { |
|
72
|
|
|
return null; |
|
73
|
|
|
} |
|
74
|
|
|
|
|
75
|
|
|
$wrap = $bag[$labelId] ?? null; |
|
76
|
|
|
if (!$wrap || !\is_object($wrap)) { |
|
77
|
|
|
return null; |
|
78
|
|
|
} |
|
79
|
|
|
|
|
80
|
|
|
// Unwrap ->obj if present |
|
81
|
|
|
$desc = (isset($wrap->obj) && \is_object($wrap->obj)) ? $wrap->obj : $wrap; |
|
82
|
|
|
|
|
83
|
|
|
$title = $this->resolveTitle($desc); |
|
84
|
|
|
$introRaw = (string) ($desc->content ?? ''); |
|
85
|
|
|
$intro = $this->normalizeContent($introRaw); |
|
86
|
|
|
|
|
87
|
|
|
// Collect files referenced by intro (so inforef can point to them) |
|
88
|
|
|
$files = $this->collectIntroFiles($introRaw, (string) ($this->course->code ?? '')); |
|
89
|
|
|
|
|
90
|
|
|
// Build the minimal dataset required by ActivityExport::createModuleXml() |
|
91
|
|
|
return [ |
|
92
|
|
|
'id' => (int) ($desc->source_id ?? $labelId), |
|
93
|
|
|
'moduleid' => (int) ($desc->source_id ?? $labelId), |
|
94
|
|
|
'modulename' => 'label', |
|
95
|
|
|
'sectionid' => $sectionId, |
|
96
|
|
|
// Use section number = section id; falls back to 0 (General) if not in LP |
|
97
|
|
|
'sectionnumber' => $sectionId, |
|
98
|
|
|
'name' => $title, |
|
99
|
|
|
'intro' => $intro, |
|
100
|
|
|
'introformat' => 1, |
|
101
|
|
|
'timemodified' => time(), |
|
102
|
|
|
'users' => [], |
|
103
|
|
|
'files' => $files, |
|
104
|
|
|
]; |
|
105
|
|
|
} |
|
106
|
|
|
|
|
107
|
|
|
/** |
|
108
|
|
|
* Title resolver with fallback by description_type. |
|
109
|
|
|
*/ |
|
110
|
|
|
private function resolveTitle(object $desc): string |
|
111
|
|
|
{ |
|
112
|
|
|
$t = trim((string) ($desc->title ?? '')); |
|
113
|
|
|
if ('' !== $t) { |
|
114
|
|
|
return $t; |
|
115
|
|
|
} |
|
116
|
|
|
$map = [1 => 'Descripción', 2 => 'Objetivos', 3 => 'Temas']; |
|
117
|
|
|
return $map[(int) ($desc->description_type ?? 0)] ?? 'Descripción'; |
|
118
|
|
|
} |
|
119
|
|
|
|
|
120
|
|
|
/** |
|
121
|
|
|
* Normalize HTML: rewrite /document/... to @@PLUGINFILE@@/<file>, including srcset, style url(...), etc. |
|
122
|
|
|
*/ |
|
123
|
|
|
private function normalizeContent(string $html): string |
|
124
|
|
|
{ |
|
125
|
|
|
if ('' === $html) { |
|
126
|
|
|
return $html; |
|
127
|
|
|
} |
|
128
|
|
|
|
|
129
|
|
|
// Handle srcset |
|
130
|
|
|
$html = (string) preg_replace_callback( |
|
131
|
|
|
'~\bsrcset\s*=\s*([\'"])(.*?)\1~is', |
|
132
|
|
|
function (array $m): string { |
|
133
|
|
|
$q = $m[1]; $val = $m[2]; |
|
134
|
|
|
$parts = array_map('trim', explode(',', $val)); |
|
135
|
|
|
foreach ($parts as &$p) { |
|
136
|
|
|
if ($p === '') { continue; } |
|
137
|
|
|
$tokens = preg_split('/\s+/', $p, -1, PREG_SPLIT_NO_EMPTY); |
|
138
|
|
|
if (!$tokens) { continue; } |
|
139
|
|
|
$url = $tokens[0]; |
|
140
|
|
|
$new = $this->rewriteDocUrl($url); |
|
141
|
|
|
if ($new !== $url) { |
|
142
|
|
|
$tokens[0] = $new; |
|
143
|
|
|
$p = implode(' ', $tokens); |
|
144
|
|
|
} |
|
145
|
|
|
} |
|
146
|
|
|
return 'srcset='.$q.implode(', ', $parts).$q; |
|
147
|
|
|
}, |
|
148
|
|
|
$html |
|
149
|
|
|
); |
|
150
|
|
|
|
|
151
|
|
|
// Generic attributes |
|
152
|
|
|
$html = (string) preg_replace_callback( |
|
153
|
|
|
'~\b(src|href|poster|data)\s*=\s*([\'"])([^\'"]+)\2~i', |
|
154
|
|
|
fn(array $m) => $m[1].'='.$m[2].$this->rewriteDocUrl($m[3]).$m[2], |
|
155
|
|
|
$html |
|
156
|
|
|
); |
|
157
|
|
|
|
|
158
|
|
|
// Inline CSS |
|
159
|
|
|
$html = (string) preg_replace_callback( |
|
160
|
|
|
'~\bstyle\s*=\s*([\'"])(.*?)\1~is', |
|
161
|
|
|
function (array $m): string { |
|
162
|
|
|
$q = $m[1]; $style = $m[2]; |
|
163
|
|
|
$style = (string) preg_replace_callback( |
|
164
|
|
|
'~url\((["\']?)([^)\'"]+)\1\)~i', |
|
165
|
|
|
fn(array $mm) => 'url('.$mm[1].$this->rewriteDocUrl($mm[2]).$mm[1].')', |
|
166
|
|
|
$style |
|
167
|
|
|
); |
|
168
|
|
|
return 'style='.$q.$style.$q; |
|
169
|
|
|
}, |
|
170
|
|
|
$html |
|
171
|
|
|
); |
|
172
|
|
|
|
|
173
|
|
|
// <style> blocks |
|
174
|
|
|
$html = (string) preg_replace_callback( |
|
175
|
|
|
'~(<style\b[^>]*>)(.*?)(</style>)~is', |
|
176
|
|
|
function (array $m): string { |
|
177
|
|
|
$open = $m[1]; $css = $m[2]; $close = $m[3]; |
|
178
|
|
|
$css = (string) preg_replace_callback( |
|
179
|
|
|
'~url\((["\']?)([^)\'"]+)\1\)~i', |
|
180
|
|
|
fn(array $mm) => 'url('.$mm[1].$this->rewriteDocUrl($mm[2]).$mm[1].')', |
|
181
|
|
|
$css |
|
182
|
|
|
); |
|
183
|
|
|
return $open.$css.$close; |
|
184
|
|
|
}, |
|
185
|
|
|
$html |
|
186
|
|
|
); |
|
187
|
|
|
|
|
188
|
|
|
return $html; |
|
189
|
|
|
} |
|
190
|
|
|
|
|
191
|
|
|
/** |
|
192
|
|
|
* Rewrite /document/... (or /courses/<code>/document/...) to @@PLUGINFILE@@/<basename>. |
|
193
|
|
|
*/ |
|
194
|
|
|
private function rewriteDocUrl(string $url): string |
|
195
|
|
|
{ |
|
196
|
|
|
if ($url === '' || str_contains($url, '@@PLUGINFILE@@')) { |
|
197
|
|
|
return $url; |
|
198
|
|
|
} |
|
199
|
|
|
if (preg_match('#/(?:courses/[^/]+/)?document(/[^?\'" )]+)#i', $url, $m)) { |
|
200
|
|
|
return '@@PLUGINFILE@@/'.basename($m[1]); |
|
201
|
|
|
} |
|
202
|
|
|
return $url; |
|
203
|
|
|
} |
|
204
|
|
|
|
|
205
|
|
|
/** |
|
206
|
|
|
* Collect referenced intro files for files.xml (component=mod_label, filearea=intro). |
|
207
|
|
|
* |
|
208
|
|
|
* @return array<int,array<string,mixed>> |
|
209
|
|
|
*/ |
|
210
|
|
|
private function collectIntroFiles(string $introHtml, string $courseCode): array |
|
211
|
|
|
{ |
|
212
|
|
|
if ($introHtml === '') { |
|
213
|
|
|
return []; |
|
214
|
|
|
} |
|
215
|
|
|
|
|
216
|
|
|
$files = []; |
|
217
|
|
|
$contextid = (int) ($this->course->info['real_id'] ?? 0); |
|
218
|
|
|
$adminId = MoodleExport::getAdminUserData()['id'] ?? ($this->getAdminUserData()['id'] ?? 0); |
|
219
|
|
|
|
|
220
|
|
|
$resources = DocumentManager::get_resources_from_source_html($introHtml); |
|
221
|
|
|
$courseInfo = api_get_course_info($courseCode); |
|
222
|
|
|
|
|
223
|
|
|
foreach ($resources as [$src]) { |
|
224
|
|
|
if (preg_match('#/document(/[^"\']+)#', $src, $matches)) { |
|
225
|
|
|
$path = $matches[1]; |
|
226
|
|
|
$docId = DocumentManager::get_document_id($courseInfo, $path); |
|
227
|
|
|
if (!$docId) { |
|
228
|
|
|
continue; |
|
229
|
|
|
} |
|
230
|
|
|
$document = DocumentManager::get_document_data_by_id($docId, $courseCode); |
|
|
|
|
|
|
231
|
|
|
if (!$document) { |
|
232
|
|
|
continue; |
|
233
|
|
|
} |
|
234
|
|
|
|
|
235
|
|
|
$contenthash = hash('sha1', basename($document['path'])); |
|
236
|
|
|
$mimetype = (new FileExport($this->course))->getMimeType($document['path']); |
|
237
|
|
|
|
|
238
|
|
|
$files[] = [ |
|
239
|
|
|
'id' => (int) $document['id'], |
|
240
|
|
|
'contenthash' => $contenthash, |
|
241
|
|
|
'contextid' => $contextid, |
|
242
|
|
|
'component' => 'mod_label', |
|
243
|
|
|
'filearea' => 'intro', |
|
244
|
|
|
'itemid' => 0, |
|
245
|
|
|
'filepath' => '/', |
|
246
|
|
|
'documentpath'=> 'document'.$document['path'], |
|
247
|
|
|
'filename' => basename($document['path']), |
|
248
|
|
|
'userid' => $adminId, |
|
249
|
|
|
'filesize' => (int) $document['size'], |
|
250
|
|
|
'mimetype' => $mimetype, |
|
251
|
|
|
'status' => 0, |
|
252
|
|
|
'timecreated' => time() - 3600, |
|
253
|
|
|
'timemodified'=> time(), |
|
254
|
|
|
'source' => (string) $document['title'], |
|
255
|
|
|
'author' => 'Unknown', |
|
256
|
|
|
'license' => 'allrightsreserved', |
|
257
|
|
|
]; |
|
258
|
|
|
} |
|
259
|
|
|
} |
|
260
|
|
|
|
|
261
|
|
|
return $files; |
|
262
|
|
|
} |
|
263
|
|
|
|
|
264
|
|
|
/** |
|
265
|
|
|
* Write label.xml for the activity. |
|
266
|
|
|
*/ |
|
267
|
|
|
private function createLabelXml(array $data, string $dir): void |
|
268
|
|
|
{ |
|
269
|
|
|
$xml = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL; |
|
270
|
|
|
$xml .= '<activity id="'.(int) $data['id'].'" moduleid="'.(int) $data['moduleid'].'" modulename="label" contextid="'.(int) ($this->course->info['real_id'] ?? 0).'">'.PHP_EOL; |
|
271
|
|
|
$xml .= ' <label id="'.(int) $data['id'].'">'.PHP_EOL; |
|
272
|
|
|
$xml .= ' <name>'.htmlspecialchars((string) $data['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8').'</name>'.PHP_EOL; |
|
273
|
|
|
$xml .= ' <intro><![CDATA['.$data['intro'].']]></intro>'.PHP_EOL; |
|
274
|
|
|
$xml .= ' <introformat>'.(int) ($data['introformat'] ?? 1).'</introformat>'.PHP_EOL; |
|
275
|
|
|
$xml .= ' <timemodified>'.(int) $data['timemodified'].'</timemodified>'.PHP_EOL; |
|
276
|
|
|
$xml .= ' </label>'.PHP_EOL; |
|
277
|
|
|
$xml .= '</activity>'; |
|
278
|
|
|
|
|
279
|
|
|
$this->createXmlFile('label', $xml, $dir); |
|
280
|
|
|
} |
|
281
|
|
|
} |
|
282
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.