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

AnnouncementsForumExport::unwrap()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 5
eloc 5
c 1
b 0
f 1
nc 6
nop 1
dl 0
loc 9
rs 9.6111
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
use Chamilo\CourseBundle\Component\CourseCopy\Moodle\Builder\MoodleExport;
9
10
use const PHP_EOL;
11
12
/**
13
 * AnnouncementsForumExport
14
 *
15
 * Exports Chamilo announcements as a Moodle "News forum" (type=news),
16
 * using the same activity skeleton used by other exporters (module.xml,
17
 * inforef.xml, optional XMLs) and the same discussions/posts layout
18
 * used by ForumExport (discussions inside forum.xml).
19
 */
20
class AnnouncementsForumExport extends ActivityExport
21
{
22
    /** Synthetic module ID default if caller passes 0 */
23
    public const DEFAULT_MODULE_ID = 48000001;
24
25
    /**
26
     * Export announcements as a News forum activity.
27
     *
28
     * @param int    $activityId Unused (kept for signature compatibility)
29
     * @param string $exportDir  Destination base directory of the export
30
     * @param int    $moduleId   Module id used to name the activity folder
31
     * @param int    $sectionId  Moodle section id where the activity will live
32
     */
33
    public function export(int $activityId, string $exportDir, int $moduleId, int $sectionId): void
34
    {
35
        $moduleId = $moduleId > 0 ? $moduleId : self::DEFAULT_MODULE_ID;
36
        $forumDir = $this->prepareActivityDirectory($exportDir, 'forum', $moduleId);
37
38
        // Build forum payload from announcements
39
        $forumData = $this->getDataFromAnnouncements($moduleId, $sectionId);
40
41
        // Primary XMLs
42
        $this->createForumXml($forumData, $forumDir);
43
        $this->createModuleXml($forumData, $forumDir);
44
        $this->createInforefXml($forumData, $forumDir);
45
46
        // Optional skeletons (keeps structure consistent)
47
        $this->createFiltersXml($forumData, $forumDir);
48
        $this->createGradesXml($forumData, $forumDir);
49
        $this->createGradeHistoryXml($forumData, $forumDir);
50
        $this->createCompletionXml($forumData, $forumDir);
51
        $this->createCommentsXml($forumData, $forumDir);
52
        $this->createCompetenciesXml($forumData, $forumDir);
53
        $this->createRolesXml($forumData, $forumDir);
54
        $this->createCalendarXml($forumData, $forumDir);
55
    }
56
57
    /** Build forum data (1 discussion per announcement). */
58
    private function getDataFromAnnouncements(int $moduleId, int $sectionId): array
59
    {
60
        $anns = $this->collectAnnouncements();
61
62
        // Use export admin user; fallback to 2 (typical Moodle admin id)
63
        $adminData = MoodleExport::getAdminUserData();
64
        $adminId   = (int) ($adminData['id'] ?? 2);
65
        if ($adminId <= 0) {
66
            $adminId = 2;
67
        }
68
69
        $threads = [];
70
        $postId = 1;
71
        $discId = 1;
72
73
        foreach ($anns as $a) {
74
            $created = (int) ($a['created_ts'] ?? time());
75
            $subject = (string) ($a['subject'] ?? 'Announcement');
76
            $message = (string) ($a['message'] ?? '');
77
78
            // One discussion per announcement, one post inside (by admin export user)
79
            $threads[] = [
80
                'id'           => $discId,
81
                'title'        => $subject,
82
                'userid'       => $adminId,
83
                'timemodified' => $created,
84
                'usermodified' => $adminId,
85
                'firstpost'    => $postId,
86
                'posts'        => [[
87
                    'id'       => $postId,
88
                    'parent'   => 0,
89
                    'userid'   => $adminId,
90
                    'created'  => $created,
91
                    'modified' => $created,
92
                    'mailed'   => 0,
93
                    'subject'  => $subject,
94
                    // Keep rich HTML safely
95
                    'message'  => $message,
96
                ]],
97
            ];
98
99
            $postId++;
100
            $discId++;
101
        }
102
103
        return [
104
            // Identity & placement
105
            'id'            => $moduleId,
106
            'moduleid'      => $moduleId,
107
            'modulename'    => 'forum',
108
            'contextid'     => (int) ($this->course->info['real_id'] ?? 0),
109
            'sectionid'     => $sectionId,
110
            'sectionnumber' => 1,
111
112
            // News forum config
113
            'name'           => 'Announcements',
114
            'description'    => '',
115
            'type'           => 'news',
116
            'forcesubscribe' => 1,
117
118
            // Timing
119
            'timecreated'  => time(),
120
            'timemodified' => time(),
121
122
            // Content
123
            'threads' => $threads,
124
125
            // Refs → drives users.xml + userinfo=1
126
            'users' => [$adminId],
127
            'files' => [],
128
        ];
129
    }
130
131
    /** Same shape as ForumExport but type=news and CDATA for HTML messages. */
132
    private function createForumXml(array $data, string $forumDir): void
133
    {
134
        $introCdata = '<![CDATA['.(string) $data['description'].']]>';
135
136
        $xml  = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
137
        $xml .= '<activity id="'.$data['id'].'" moduleid="'.$data['moduleid'].'" modulename="forum" contextid="'.$data['contextid'].'">'.PHP_EOL;
138
        $xml .= '  <forum id="'.$data['id'].'">'.PHP_EOL;
139
        $xml .= '    <type>'.htmlspecialchars((string) ($data['type'] ?? 'news')).'</type>'.PHP_EOL;
140
        $xml .= '    <name>'.htmlspecialchars((string) $data['name']).'</name>'.PHP_EOL;
141
        $xml .= '    <intro>'.$introCdata.'</intro>'.PHP_EOL;
142
        $xml .= '    <introformat>1</introformat>'.PHP_EOL;
143
        $xml .= '    <duedate>0</duedate>'.PHP_EOL;
144
        $xml .= '    <cutoffdate>0</cutoffdate>'.PHP_EOL;
145
        $xml .= '    <assessed>0</assessed>'.PHP_EOL;
146
        $xml .= '    <assesstimestart>0</assesstimestart>'.PHP_EOL;
147
        $xml .= '    <assesstimefinish>0</assesstimefinish>'.PHP_EOL;
148
        $xml .= '    <scale>100</scale>'.PHP_EOL;
149
        $xml .= '    <maxbytes>512000</maxbytes>'.PHP_EOL;
150
        $xml .= '    <maxattachments>9</maxattachments>'.PHP_EOL;
151
        $xml .= '    <forcesubscribe>'.(int) ($data['forcesubscribe'] ?? 1).'</forcesubscribe>'.PHP_EOL;
152
        $xml .= '    <trackingtype>1</trackingtype>'.PHP_EOL;
153
        $xml .= '    <rsstype>0</rsstype>'.PHP_EOL;
154
        $xml .= '    <rssarticles>0</rssarticles>'.PHP_EOL;
155
        $xml .= '    <timemodified>'.$data['timemodified'].'</timemodified>'.PHP_EOL;
156
        $xml .= '    <warnafter>0</warnafter>'.PHP_EOL;
157
        $xml .= '    <blockafter>0</blockafter>'.PHP_EOL;
158
        $xml .= '    <blockperiod>0</blockperiod>'.PHP_EOL;
159
        $xml .= '    <completiondiscussions>0</completiondiscussions>'.PHP_EOL;
160
        $xml .= '    <completionreplies>0</completionreplies>'.PHP_EOL;
161
        $xml .= '    <completionposts>0</completionposts>'.PHP_EOL;
162
        $xml .= '    <displaywordcount>0</displaywordcount>'.PHP_EOL;
163
        $xml .= '    <lockdiscussionafter>0</lockdiscussionafter>'.PHP_EOL;
164
        $xml .= '    <grade_forum>0</grade_forum>'.PHP_EOL;
165
166
        $xml .= '    <discussions>'.PHP_EOL;
167
        foreach ($data['threads'] as $thread) {
168
            $xml .= '      <discussion id="'.$thread['id'].'">'.PHP_EOL;
169
            $xml .= '        <name>'.htmlspecialchars((string) $thread['title']).'</name>'.PHP_EOL;
170
            $xml .= '        <firstpost>'.(int) $thread['firstpost'].'</firstpost>'.PHP_EOL;
171
            $xml .= '        <userid>'.$thread['userid'].'</userid>'.PHP_EOL;
172
            $xml .= '        <groupid>-1</groupid>'.PHP_EOL;
173
            $xml .= '        <assessed>0</assessed>'.PHP_EOL;
174
            $xml .= '        <timemodified>'.$thread['timemodified'].'</timemodified>'.PHP_EOL;
175
            $xml .= '        <usermodified>'.$thread['usermodified'].'</usermodified>'.PHP_EOL;
176
            $xml .= '        <timestart>0</timestart>'.PHP_EOL;
177
            $xml .= '        <timeend>0</timeend>'.PHP_EOL;
178
            $xml .= '        <pinned>0</pinned>'.PHP_EOL;
179
            $xml .= '        <timelocked>0</timelocked>'.PHP_EOL;
180
181
            $xml .= '        <posts>'.PHP_EOL;
182
            foreach ($thread['posts'] as $post) {
183
                $xml .= '          <post id="'.$post['id'].'">'.PHP_EOL;
184
                $xml .= '            <parent>'.(int) $post['parent'].'</parent>'.PHP_EOL;
185
                $xml .= '            <userid>'.$post['userid'].'</userid>'.PHP_EOL;
186
                $xml .= '            <created>'.$post['created'].'</created>'.PHP_EOL;
187
                $xml .= '            <modified>'.$post['modified'].'</modified>'.PHP_EOL;
188
                $xml .= '            <mailed>'.(int) $post['mailed'].'</mailed>'.PHP_EOL;
189
                $xml .= '            <subject>'.htmlspecialchars((string) $post['subject']).'</subject>'.PHP_EOL;
190
                $xml .= '            <message><![CDATA['.$post['message'].']]></message>'.PHP_EOL;
191
                $xml .= '            <messageformat>1</messageformat>'.PHP_EOL;
192
                $xml .= '            <messagetrust>0</messagetrust>'.PHP_EOL;
193
                $xml .= '            <attachment></attachment>'.PHP_EOL;
194
                $xml .= '            <totalscore>0</totalscore>'.PHP_EOL;
195
                $xml .= '            <mailnow>0</mailnow>'.PHP_EOL;
196
                $xml .= '            <privatereplyto>0</privatereplyto>'.PHP_EOL;
197
                $xml .= '            <ratings></ratings>'.PHP_EOL;
198
                $xml .= '          </post>'.PHP_EOL;
199
            }
200
            $xml .= '        </posts>'.PHP_EOL;
201
202
            $xml .= '        <discussion_subs>'.PHP_EOL;
203
            $xml .= '          <discussion_sub id="'.$thread['id'].'">'.PHP_EOL;
204
            $xml .= '            <userid>'.$thread['userid'].'</userid>'.PHP_EOL;
205
            $xml .= '            <preference>'.$thread['timemodified'].'</preference>'.PHP_EOL;
206
            $xml .= '          </discussion_sub>'.PHP_EOL;
207
            $xml .= '        </discussion_subs>'.PHP_EOL;
208
209
            $xml .= '      </discussion>'.PHP_EOL;
210
        }
211
        $xml .= '    </discussions>'.PHP_EOL;
212
213
        $xml .= '  </forum>'.PHP_EOL;
214
        $xml .= '</activity>';
215
216
        $this->createXmlFile('forum', $xml, $forumDir);
217
    }
218
219
    /**
220
     * Collect announcements from CourseBuilder bag.
221
     *
222
     * Supports multiple bucket names and shapes defensively:
223
     * - resources[RESOURCE_ANNOUNCEMENT] or resources['announcements'] or ['announcement']
224
     * - items wrapped as {obj: …} or direct objects/arrays
225
     */
226
    private function collectAnnouncements(): array
227
    {
228
        $res = \is_array($this->course->resources ?? null) ? $this->course->resources : [];
229
230
        $bag =
231
            ($res[\defined('RESOURCE_ANNOUNCEMENT') ? RESOURCE_ANNOUNCEMENT : 'announcements'] ?? null)
232
            ?? ($res['announcements'] ?? null)
233
            ?? ($res['announcement'] ?? null)
234
            ?? [];
235
236
        $out = [];
237
        foreach ((array) $bag as $maybe) {
238
            $o = $this->unwrap($maybe);
239
            if (!$o) { continue; }
240
241
            $title = $this->firstNonEmpty($o, ['title','name','subject'], 'Announcement');
242
            $html  = $this->firstNonEmpty($o, ['content','message','description','text','body'], '');
243
            if ($html === '') { continue; }
244
245
            $ts = $this->firstTimestamp($o, ['created','ctime','date','add_date','time']);
246
            $out[] = ['subject' => $title, 'message' => $html, 'created_ts' => $ts];
247
        }
248
249
        return $out;
250
    }
251
252
    private function unwrap(mixed $maybe): ?object
253
    {
254
        if (\is_object($maybe)) {
255
            return (isset($maybe->obj) && \is_object($maybe->obj)) ? $maybe->obj : $maybe;
256
        }
257
        if (\is_array($maybe)) {
258
            return (object) $maybe;
259
        }
260
        return null;
261
    }
262
263
    private function firstNonEmpty(object $o, array $keys, string $fallback = ''): string
264
    {
265
        foreach ($keys as $k) {
266
            if (!empty($o->{$k}) && \is_string($o->{$k})) {
267
                $v = trim((string) $o->{$k});
268
                if ($v !== '') { return $v; }
269
            }
270
        }
271
        return $fallback;
272
    }
273
274
    private function firstTimestamp(object $o, array $keys): int
275
    {
276
        foreach ($keys as $k) {
277
            if (isset($o->{$k})) {
278
                $v = $o->{$k};
279
                if (\is_numeric($v)) { return (int) $v; }
280
                if (\is_string($v)) {
281
                    $t = strtotime($v);
282
                    if (false !== $t) { return (int) $t; }
283
                }
284
            }
285
        }
286
        return time();
287
    }
288
}
289