Passed
Pull Request — master (#7027)
by
unknown
12:49 queued 03:07
created

CourseCalendarExport   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 141
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 86
c 1
b 0
f 1
dl 0
loc 141
rs 9.6
wmc 35

8 Methods

Rating   Name   Duplication   Size   Complexity  
B collectEvents() 0 41 7
A unwrap() 0 5 5
A buildEventsXml() 0 26 2
A export() 0 18 3
A firstTimestamp() 0 9 6
A firstNonEmpty() 0 9 5
A __construct() 0 3 1
A firstTimestampOrNull() 0 9 6
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');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for error_log(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

21
            /** @scrutinizer ignore-unhandled */ @error_log('[CourseCalendarExport] No events found; skipping course/events.xml');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
22
            return 0;
23
        }
24
25
        $courseDir = rtrim($exportDir, '/') . '/course';
26
        if (!is_dir($courseDir)) {
27
            @mkdir($courseDir, 0775, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

27
            /** @scrutinizer ignore-unhandled */ @mkdir($courseDir, 0775, true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
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