|
1
|
|
|
<?php |
|
2
|
|
|
/* For licensing terms, see /license.txt */ |
|
3
|
|
|
declare(strict_types=1); |
|
4
|
|
|
|
|
5
|
|
|
namespace Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities; |
|
6
|
|
|
|
|
7
|
|
|
final class CourseCalendarExport |
|
8
|
|
|
{ |
|
9
|
|
|
private object $course; |
|
10
|
|
|
|
|
11
|
|
|
public function __construct(object $course) |
|
12
|
|
|
{ |
|
13
|
|
|
$this->course = $course; |
|
14
|
|
|
} |
|
15
|
|
|
|
|
16
|
|
|
/** Export course-level calendar events into course/events.xml */ |
|
17
|
|
|
public function export(string $exportDir): int |
|
18
|
|
|
{ |
|
19
|
|
|
$events = $this->collectEvents(); |
|
20
|
|
|
if (empty($events)) { |
|
21
|
|
|
@error_log('[CourseCalendarExport] No events found; skipping course/events.xml'); |
|
|
|
|
|
|
22
|
|
|
return 0; |
|
23
|
|
|
} |
|
24
|
|
|
|
|
25
|
|
|
$courseDir = rtrim($exportDir, '/') . '/course'; |
|
26
|
|
|
if (!is_dir($courseDir)) { |
|
27
|
|
|
@mkdir($courseDir, 0775, true); |
|
|
|
|
|
|
28
|
|
|
} |
|
29
|
|
|
|
|
30
|
|
|
$xml = $this->buildEventsXml($events); |
|
31
|
|
|
file_put_contents($courseDir . '/events.xml', $xml); |
|
32
|
|
|
@error_log('[CourseCalendarExport] Wrote '.count($events).' events to course/events.xml'); |
|
33
|
|
|
|
|
34
|
|
|
return count($events); |
|
35
|
|
|
} |
|
36
|
|
|
|
|
37
|
|
|
/** Collect events from legacy course resources (best-effort). */ |
|
38
|
|
|
private function collectEvents(): array |
|
39
|
|
|
{ |
|
40
|
|
|
$res = \is_array($this->course->resources ?? null) ? $this->course->resources : []; |
|
41
|
|
|
$bag = ($res[\defined('RESOURCE_EVENT') ? RESOURCE_EVENT : 'events'] ?? null) |
|
42
|
|
|
?? ($res['events'] ?? null) |
|
43
|
|
|
?? ($res['event'] ?? null) |
|
44
|
|
|
?? ($res['agenda'] ?? null) |
|
45
|
|
|
?? []; |
|
46
|
|
|
|
|
47
|
|
|
$out = []; |
|
48
|
|
|
foreach ((array) $bag as $maybe) { |
|
49
|
|
|
$o = $this->unwrap($maybe); |
|
50
|
|
|
if (!$o) continue; |
|
51
|
|
|
|
|
52
|
|
|
$title = $this->firstNonEmpty($o, ['title','name','subject'], 'Event'); |
|
53
|
|
|
$desc = $this->firstNonEmpty($o, ['content','description','text','body'], ''); |
|
54
|
|
|
$ts = $this->firstTimestamp($o, ['start','start_date','from','begin','date']); |
|
55
|
|
|
$te = $this->firstTimestampOrNull($o, ['end','end_date','to','due']); |
|
56
|
|
|
$dur = ($te !== null && $te > $ts) ? ($te - $ts) : 0; |
|
57
|
|
|
|
|
58
|
|
|
$out[] = [ |
|
59
|
|
|
'name' => $title, |
|
60
|
|
|
'description' => $desc, |
|
61
|
|
|
'format' => 1, |
|
62
|
|
|
// Let Moodle bind on restore: |
|
63
|
|
|
'courseid' => '$@NULL@$', // important |
|
64
|
|
|
'groupid' => 0, |
|
65
|
|
|
'userid' => 0, // restoring without users |
|
66
|
|
|
'repeatid' => 0, |
|
67
|
|
|
'eventtype' => 'course', |
|
68
|
|
|
'timestart' => $ts, |
|
69
|
|
|
'timeduration' => $dur, |
|
70
|
|
|
'visible' => 1, |
|
71
|
|
|
'timemodified' => $ts, |
|
72
|
|
|
'timesort' => $ts, |
|
73
|
|
|
]; |
|
74
|
|
|
} |
|
75
|
|
|
|
|
76
|
|
|
// Sort by timestart for determinism |
|
77
|
|
|
usort($out, static fn($a,$b) => [$a['timestart'],$a['name']] <=> [$b['timestart'],$b['name']]); |
|
78
|
|
|
return $out; |
|
79
|
|
|
} |
|
80
|
|
|
|
|
81
|
|
|
/** Build the minimal, Moodle-compatible events.xml */ |
|
82
|
|
|
private function buildEventsXml(array $events): string |
|
83
|
|
|
{ |
|
84
|
|
|
$eol = PHP_EOL; |
|
85
|
|
|
$xml = '<?xml version="1.0" encoding="UTF-8"?>'.$eol; |
|
86
|
|
|
$xml .= '<events>'.$eol; |
|
87
|
|
|
|
|
88
|
|
|
foreach ($events as $e) { |
|
89
|
|
|
$xml .= ' <event>'.$eol; |
|
90
|
|
|
$xml .= ' <name>'.htmlspecialchars($e['name'], ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8').'</name>'.$eol; |
|
91
|
|
|
$xml .= ' <description><![CDATA['.($e['description'] ?? '').']]></description>'.$eol; |
|
92
|
|
|
$xml .= ' <format>'.(int)$e['format'].'</format>'.$eol; |
|
93
|
|
|
$xml .= ' <courseid>'.$e['courseid'].'</courseid>'.$eol; // $@NULL@$ |
|
94
|
|
|
$xml .= ' <groupid>'.(int)$e['groupid'].'</groupid>'.$eol; |
|
95
|
|
|
$xml .= ' <userid>'.(int)$e['userid'].'</userid>'.$eol; |
|
96
|
|
|
$xml .= ' <repeatid>'.(int)$e['repeatid'].'</repeatid>'.$eol; |
|
97
|
|
|
$xml .= ' <eventtype>'.htmlspecialchars($e['eventtype'], ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8').'</eventtype>'.$eol; |
|
98
|
|
|
$xml .= ' <timestart>'.(int)$e['timestart'].'</timestart>'.$eol; |
|
99
|
|
|
$xml .= ' <timeduration>'.(int)$e['timeduration'].'</timeduration>'.$eol; |
|
100
|
|
|
$xml .= ' <visible>'.(int)$e['visible'].'</visible>'.$eol; |
|
101
|
|
|
$xml .= ' <timemodified>'.(int)$e['timemodified'].'</timemodified>'.$eol; |
|
102
|
|
|
$xml .= ' <timesort>'.(int)$e['timesort'].'</timesort>'.$eol; |
|
103
|
|
|
$xml .= ' </event>'.$eol; |
|
104
|
|
|
} |
|
105
|
|
|
|
|
106
|
|
|
$xml .= '</events>'.$eol; |
|
107
|
|
|
return $xml; |
|
108
|
|
|
} |
|
109
|
|
|
|
|
110
|
|
|
private function unwrap(mixed $x): ?object |
|
111
|
|
|
{ |
|
112
|
|
|
if (\is_object($x)) return isset($x->obj) && \is_object($x->obj) ? $x->obj : $x; |
|
113
|
|
|
if (\is_array($x)) return (object) $x; |
|
114
|
|
|
return null; |
|
115
|
|
|
} |
|
116
|
|
|
|
|
117
|
|
|
private function firstNonEmpty(object $o, array $keys, string $fallback=''): string |
|
118
|
|
|
{ |
|
119
|
|
|
foreach ($keys as $k) { |
|
120
|
|
|
if (!empty($o->{$k}) && \is_string($o->{$k})) { |
|
121
|
|
|
$v = trim((string)$o->{$k}); |
|
122
|
|
|
if ($v !== '') return $v; |
|
123
|
|
|
} |
|
124
|
|
|
} |
|
125
|
|
|
return $fallback; |
|
126
|
|
|
} |
|
127
|
|
|
|
|
128
|
|
|
private function firstTimestamp(object $o, array $keys): int |
|
129
|
|
|
{ |
|
130
|
|
|
foreach ($keys as $k) { |
|
131
|
|
|
if (!isset($o->{$k})) continue; |
|
132
|
|
|
$v = $o->{$k}; |
|
133
|
|
|
if (\is_numeric($v)) return (int)$v; |
|
134
|
|
|
if (\is_string($v)) { $t = strtotime($v); if ($t !== false) return (int)$t; } |
|
135
|
|
|
} |
|
136
|
|
|
return time(); |
|
137
|
|
|
} |
|
138
|
|
|
|
|
139
|
|
|
private function firstTimestampOrNull(object $o, array $keys): ?int |
|
140
|
|
|
{ |
|
141
|
|
|
foreach ($keys as $k) { |
|
142
|
|
|
if (!isset($o->{$k})) continue; |
|
143
|
|
|
$v = $o->{$k}; |
|
144
|
|
|
if (\is_numeric($v)) return (int)$v; |
|
145
|
|
|
if (\is_string($v)) { $t = strtotime($v); if ($t !== false) return (int)$t; } |
|
146
|
|
|
} |
|
147
|
|
|
return null; |
|
148
|
|
|
} |
|
149
|
|
|
} |
|
150
|
|
|
|
If you suppress an error, we recommend checking for the error condition explicitly: