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

AttendanceMetaExport::export()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 19
c 1
b 0
f 1
nc 3
nop 4
dl 0
loc 35
rs 9.6333
1
<?php
2
/* For licensing terms, see /license.txt */
3
4
declare(strict_types=1);
5
6
namespace Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities;
7
8
/**
9
 * AttendanceMetaExport
10
 *
11
 * Writes Chamilo-only metadata for Attendance into:
12
 *   chamilo/attendance/attendance_{moduleId}.json
13
 * and indexes it from chamilo/manifest.json. Moodle will ignore this directory,
14
 * while Chamilo can re-import it losslessly.
15
 *
16
 * Data source: CourseBuilder legacy bag (RESOURCE_ATTENDANCE).
17
 */
18
class AttendanceMetaExport extends ActivityExport
19
{
20
    /**
21
     * Export one Attendance as JSON + manifest entry.
22
     *
23
     * @param int    $activityId Legacy id (iid) of the attendance activity (from CourseBuilder)
24
     * @param string $exportDir  Absolute temp export directory for the .mbz (root of backup)
25
     * @param int    $moduleId   Module id used to name directories/files inside activities/
26
     * @param int    $sectionId  Section id (topic), informative for manifest
27
     */
28
    public function export(int $activityId, string $exportDir, int $moduleId, int $sectionId): void
29
    {
30
        $attendance = $this->findAttendanceById($activityId);
31
        if (null === $attendance) {
32
            // Nothing to export; keep a trace for debug
33
            @error_log('[AttendanceMetaExport] Skipping: attendance not found id='.$activityId);
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

33
            /** @scrutinizer ignore-unhandled */ @error_log('[AttendanceMetaExport] Skipping: attendance not found id='.$activityId);

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...
34
            return;
35
        }
36
37
        // Build payload from legacy object assembled by CourseBuilder
38
        $payload = $this->buildPayloadFromLegacy($attendance, $moduleId, $sectionId);
39
40
        // Ensure base dir exists: {exportDir}/chamilo/attendance
41
        $base = rtrim($exportDir, '/').'/chamilo/attendance';
42
        if (!is_dir($base)) {
43
            @mkdir($base, (int) octdec('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

43
            /** @scrutinizer ignore-unhandled */ @mkdir($base, (int) octdec('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...
44
        }
45
46
        // Write JSON: chamilo/attendance/attendance_{moduleId}.json
47
        $jsonFile = $base.'/attendance_'.$moduleId.'.json';
48
        file_put_contents(
49
            $jsonFile,
50
            json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
51
        );
52
53
        // Append entry to chamilo/manifest.json
54
        $this->appendToManifest($exportDir, [
55
            'kind'      => 'attendance',
56
            'moduleid'  => $moduleId,
57
            'sectionid' => $sectionId,
58
            'title'     => (string) ($payload['name'] ?? 'Attendance'),
59
            'path'      => 'chamilo/attendance/attendance_'.$moduleId.'.json',
60
        ]);
61
62
        @error_log('[AttendanceMetaExport] Exported attendance moduleid='.$moduleId.' sectionid='.$sectionId);
63
    }
64
65
    /**
66
     * Find an Attendance legacy object from the CourseBuilder bag.
67
     *
68
     * Accepts multiple buckets and wrappers defensively:
69
     * - resources[RESOURCE_ATTENDANCE] or resources['attendance']
70
     * - each item may be {$obj: …} or the object itself.
71
     */
72
    private function findAttendanceById(int $iid): ?object
73
    {
74
        $bag = $this->course->resources[\defined('RESOURCE_ATTENDANCE') ? RESOURCE_ATTENDANCE : 'attendance']
75
            ?? $this->course->resources['attendance']
76
            ?? [];
77
78
        if (!\is_array($bag)) {
79
            return null;
80
        }
81
82
        foreach ($bag as $maybe) {
83
            if (!\is_object($maybe)) {
84
                continue;
85
            }
86
            $obj = (isset($maybe->obj) && \is_object($maybe->obj)) ? $maybe->obj : $maybe;
87
88
            // Accept id, iid or source_id (defensive)
89
            $candidates = [
90
                (int) ($obj->id ?? 0),
91
                (int) ($obj->iid ?? 0),
92
                (int) ($obj->source_id ?? 0),
93
            ];
94
            if (\in_array($iid, $candidates, true)) {
95
                return $obj;
96
            }
97
        }
98
99
        return null;
100
    }
101
102
    /**
103
     * Build a robust JSON payload from the legacy Attendance object.
104
     * Tries several field names to be resilient to legacy structures.
105
     */
106
    private function buildPayloadFromLegacy(object $att, int $moduleId, int $sectionId): array
107
    {
108
        $name = $this->firstNonEmptyString($att, ['title','name'], 'Attendance');
109
        $intro = $this->firstNonEmptyString($att, ['description','intro','introtext'], '');
110
        $active = (int) ($att->active ?? 1);
111
112
        $qualTitle = $this->firstNonEmptyString($att, ['attendance_qualify_title','grade_title'], '');
113
        $qualMax   = (int) ($att->attendance_qualify_max ?? $att->grade_max ?? 0);
114
        $weight    = (float) ($att->attendance_weight ?? 0.0);
115
        $locked    = (int) ($att->locked ?? 0);
116
117
        $calendars = $this->extractCalendars($att);
118
119
        return [
120
            'type'        => 'attendance',
121
            'moduleid'    => $moduleId,
122
            'sectionid'   => $sectionId,
123
            'name'        => $name,
124
            'intro'       => $intro,
125
            'active'      => $active,
126
            'qualify'     => [
127
                'title' => $qualTitle,
128
                'max'   => $qualMax,
129
                'weight'=> $weight,
130
            ],
131
            'locked'      => $locked,
132
            'calendars'   => $calendars,
133
            '_exportedAt' => date('c'),
134
        ];
135
    }
136
137
    /** Extract calendars list from different possible shapes. */
138
    private function extractCalendars(object $att): array
139
    {
140
        // Try common property names first
141
        $lists = [
142
            $att->calendars          ?? null,
143
            $att->attendance_calendar?? null,
144
            $att->calendar           ?? null,
145
        ];
146
147
        // Try getter methods as fallback
148
        foreach (['getCalendars','get_calendar','get_attendance_calendars'] as $m) {
149
            if (\is_callable([$att, $m])) {
150
                $lists[] = $att->{$m}();
151
            }
152
        }
153
154
        // Flatten items to a normalized array
155
        $out = [];
156
        foreach ($lists as $maybeList) {
157
            if (!$maybeList) {
158
                continue;
159
            }
160
            foreach ((array) $maybeList as $c) {
161
                if (!\is_array($c) && !\is_object($c)) {
162
                    continue;
163
                }
164
                $id     = (int) ($c['id'] ?? $c['iid'] ?? $c->id ?? $c->iid ?? 0);
165
                $aid    = (int) ($c['attendance_id'] ?? $c->attendance_id ?? 0);
166
                $dt     = (string) ($c['date_time'] ?? $c->date_time ?? $c['datetime'] ?? $c->datetime ?? '');
167
                $done   = (bool)  ($c['done_attendance'] ?? $c->done_attendance ?? false);
168
                $blocked= (bool)  ($c['blocked'] ?? $c->blocked ?? false);
169
                $dur    = $c['duration'] ?? $c->duration ?? null;
170
                $dur    = (null !== $dur) ? (int) $dur : null;
171
172
                $out[$id] = [
173
                    'id'             => $id,
174
                    'attendance_id'  => $aid,
175
                    'date_time'      => $dt,
176
                    'done_attendance'=> $done,
177
                    'blocked'        => $blocked,
178
                    'duration'       => $dur,
179
                ];
180
            }
181
        }
182
183
        // Preserve stable order
184
        ksort($out);
185
186
        return array_values($out);
187
    }
188
189
    /** Helper: pick first non-empty string field from object. */
190
    private function firstNonEmptyString(object $o, array $keys, string $fallback = ''): string
191
    {
192
        foreach ($keys as $k) {
193
            if (!empty($o->{$k}) && \is_string($o->{$k})) {
194
                $v = trim((string) $o->{$k});
195
                if ($v !== '') {
196
                    return $v;
197
                }
198
            }
199
        }
200
        return $fallback;
201
    }
202
203
    /** Append a record into chamilo/manifest.json (create if missing). */
204
    private function appendToManifest(string $exportDir, array $record): void
205
    {
206
        $dir = rtrim($exportDir, '/').'/chamilo';
207
        if (!is_dir($dir)) {
208
            @mkdir($dir, (int) octdec('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

208
            /** @scrutinizer ignore-unhandled */ @mkdir($dir, (int) octdec('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...
209
        }
210
211
        $manifestFile = $dir.'/manifest.json';
212
        $manifest = [
213
            'version'     => 1,
214
            'exporter'    => 'C2-MoodleExport',
215
            'generatedAt' => date('c'),
216
            'items'       => [],
217
        ];
218
219
        if (is_file($manifestFile)) {
220
            $decoded = json_decode((string) file_get_contents($manifestFile), true);
221
            if (\is_array($decoded)) {
222
                $manifest = array_replace_recursive($manifest, $decoded);
223
            }
224
            if (!isset($manifest['items']) || !\is_array($manifest['items'])) {
225
                $manifest['items'] = [];
226
            }
227
        }
228
229
        $manifest['items'][] = $record;
230
231
        file_put_contents(
232
            $manifestFile,
233
            json_encode($manifest, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
234
        );
235
    }
236
}
237