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

WikiExport   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Importance

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

7 Methods

Rating   Name   Duplication   Size   Complexity  
B createWikiXml() 0 68 5
C getData() 0 72 13
A export() 0 24 2
B normalizeContent() 0 66 6
A rewriteDocUrl() 0 9 4
A h() 0 3 1
A toTimestamp() 0 6 4
1
<?php
2
/* For license 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 ENT_QUOTES;
11
use const ENT_SUBSTITUTE;
12
use const PHP_EOL;
13
14
/**
15
 * WikiExport — exports legacy CWiki pages as Moodle "wiki" activities.
16
 * - Writes activities/wiki_{moduleId}/{wiki.xml,module.xml,inforef.xml,...}
17
 * - One Chamilo wiki page => one Moodle wiki activity (single subwiki + single page + version #1).
18
 * - Keeps the same auxiliary XMLs consistency as LabelExport (module.xml, inforef.xml, etc.).
19
 */
20
final class WikiExport extends ActivityExport
21
{
22
    /**
23
     * Export a single Wiki activity.
24
     *
25
     * @param int    $activityId Source page identifier (we try pageId, iid, or array key)
26
     * @param string $exportDir  Root temp export directory
27
     * @param int    $moduleId   Module id used in directory name (usually = $activityId)
28
     * @param int    $sectionId  Resolved course section (0 = General)
29
     */
30
    public function export(int $activityId, string $exportDir, int $moduleId, int $sectionId): void
31
    {
32
        $wikiDir = $this->prepareActivityDirectory($exportDir, 'wiki', $moduleId);
33
34
        $data = $this->getData($activityId, $sectionId);
35
        if (null === $data) {
36
            // Nothing to export
37
            return;
38
        }
39
40
        // Primary XMLs
41
        $this->createWikiXml($data, $wikiDir);     // activities/wiki_{id}/wiki.xml
42
        $this->createModuleXml($data, $wikiDir);   // activities/wiki_{id}/module.xml
43
        $this->createInforefXml($data, $wikiDir);  // activities/wiki_{id}/inforef.xml
44
45
        // Optional auxiliaries to keep structure consistent with other exporters
46
        $this->createFiltersXml($data, $wikiDir);
47
        $this->createGradesXml($data, $wikiDir);
48
        $this->createGradeHistoryXml($data, $wikiDir);
49
        $this->createCompletionXml($data, $wikiDir);
50
        $this->createCommentsXml($data, $wikiDir);
51
        $this->createCompetenciesXml($data, $wikiDir);
52
        $this->createRolesXml($data, $wikiDir);
53
        $this->createCalendarXml($data, $wikiDir);
54
    }
55
56
    /**
57
     * Build wiki payload from legacy "wiki" bucket (CWiki).
58
     *
59
     * Returns a structure with:
60
     * - id, moduleid, modulename='wiki', sectionid, sectionnumber
61
     * - name (title), intro (empty), introformat=1 (HTML)
62
     * - wikimode ('collaborative'), defaultformat ('html'), forceformat=1
63
     * - firstpagetitle, timecreated, timemodified
64
     * - pages[]: one page with versions[0]
65
     */
66
    public function getData(int $activityId, int $sectionId): ?array
67
    {
68
        $bag =
69
            $this->course->resources[\defined('RESOURCE_WIKI') ? RESOURCE_WIKI : 'wiki']
70
            ?? $this->course->resources['wiki']
71
            ?? [];
72
73
        if (empty($bag) || !\is_array($bag)) {
74
            return null;
75
        }
76
77
        $pages  = [];
78
        $users  = [];
79
        $firstTitle = null;
80
81
        foreach ($bag as $key => $wrap) {
82
            if (!\is_object($wrap)) { continue; }
83
            $p = (isset($wrap->obj) && \is_object($wrap->obj)) ? $wrap->obj : $wrap;
84
85
            $pid = (int)($p->pageId ?? $p->iid ?? $key ?? 0);
86
            if ($pid <= 0) { continue; }
87
88
            $title = trim((string)($p->title ?? 'Wiki page '.$pid));
89
            if ($title === '') { $title = 'Wiki page '.$pid; }
90
            $rawHtml  = (string)($p->content ?? '');
91
            $content  = $this->normalizeContent($rawHtml);
92
93
            $userId   = (int)($p->userId ?? 0);
94
            $created  = $this->toTimestamp((string)($p->dtime ?? ''), time());
95
            $modified = $created;
96
97
            $pages[] = [
98
                'id'            => $pid,
99
                'title'         => $title,
100
                'content'       => $content,
101
                'contentformat' => 'html',
102
                'version'       => 1,
103
                'timecreated'   => $created,
104
                'timemodified'  => $modified,
105
                'userid'        => $userId,
106
            ];
107
108
            if ($userId > 0) { $users[$userId] = true; }
109
            if (null === $firstTitle) { $firstTitle = $title; }
110
        }
111
112
        if (empty($pages)) {
113
            return null;
114
        }
115
116
        return [
117
            'id'            => $activityId,
118
            'moduleid'      => $activityId,
119
            'modulename'    => 'wiki',
120
            'sectionid'     => $sectionId,
121
            'sectionnumber' => $sectionId,
122
            'name'          => 'Wiki',
123
            'intro'         => '',
124
            'introformat'   => 1,
125
            'timemodified'  => max(array_column($pages, 'timemodified')),
126
            'editbegin'     => 0,
127
            'editend'       => 0,
128
129
            'wikimode'        => 'collaborative',
130
            'defaultformat'   => 'html',
131
            'forceformat'     => 1,
132
            'firstpagetitle'  => $firstTitle ?? 'Home',
133
            'timecreated'     => min(array_column($pages, 'timecreated')),
134
            'timemodified2'   => max(array_column($pages, 'timemodified')),
135
136
            'pages'           => $pages,
137
            'userids'         => array_keys($users),
138
        ];
139
    }
140
141
    /**
142
     * Write activities/wiki_{id}/wiki.xml
143
     * NOTE: We ensure non-null <cachedcontent> and present <userid> at <page> level
144
     * to satisfy Moodle restore expectations (mdl_wiki_pages.userid and cachedcontent NOT NULL).
145
     */
146
    private function createWikiXml(array $d, string $dir): void
147
    {
148
        $admin = MoodleExport::getAdminUserData();
149
        $adminId = (int)($admin['id'] ?? 2);
150
        if ($adminId <= 0) { $adminId = 2; }
151
152
        $xml  = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
153
        $xml .= '<activity id="'.(int)$d['id'].'" moduleid="'.(int)$d['moduleid'].'" modulename="wiki" contextid="'.(int)($this->course->info['real_id'] ?? 0).'">'.PHP_EOL;
154
        $xml .= '  <wiki id="'.(int)$d['id'].'">'.PHP_EOL;
155
        $xml .= '    <name>'.$this->h($d['name']).'</name>'.PHP_EOL;
156
        $xml .= '    <intro><![CDATA['.$d['intro'].']]></intro>'.PHP_EOL;
157
        $xml .= '    <introformat>'.(int)($d['introformat'] ?? 1).'</introformat>'.PHP_EOL;
158
        $xml .= '    <wikimode>'.$this->h((string)$d['wikimode']).'</wikimode>'.PHP_EOL;
159
        $xml .= '    <defaultformat>'.$this->h((string)$d['defaultformat']).'</defaultformat>'.PHP_EOL;
160
        $xml .= '    <forceformat>'.(int)$d['forceformat'].'</forceformat>'.PHP_EOL;
161
        $xml .= '    <firstpagetitle>'.$this->h((string)$d['firstpagetitle']).'</firstpagetitle>'.PHP_EOL;
162
        $xml .= '    <timecreated>'.(int)$d['timecreated'].'</timecreated>'.PHP_EOL;
163
        $xml .= '    <timemodified>'.(int)$d['timemodified2'].'</timemodified>'.PHP_EOL;
164
        $xml .= '    <editbegin>'.(int)($d['editbegin'] ?? 0).'</editbegin>'.PHP_EOL;
165
        $xml .= '    <editend>'.(int)($d['editend'] ?? 0).'</editend>'.PHP_EOL;
166
167
        // single subwiki (no groups/users)
168
        $xml .= '    <subwikis>'.PHP_EOL;
169
        $xml .= '      <subwiki id="1">'.PHP_EOL;
170
        $xml .= '        <groupid>0</groupid>'.PHP_EOL;
171
        $xml .= '        <userid>0</userid>'.PHP_EOL;
172
173
        // pages
174
        $xml .= '        <pages>'.PHP_EOL;
175
        foreach ($d['pages'] as $i => $p) {
176
            $pid = (int)$p['id'];
177
            $pageUserId = (int)($p['userid'] ?? 0);
178
            if ($pageUserId <= 0) { $pageUserId = $adminId; } // fallback user id
179
180
            // Ensure non-empty cachedcontent; Moodle expects NOT NULL.
181
            $pageHtml = trim((string)($p['content'] ?? ''));
182
            if ($pageHtml === '') { $pageHtml = '<p></p>'; }
183
184
            $xml .= '          <page id="'.$pid.'">'.PHP_EOL;
185
            $xml .= '            <title>'.$this->h((string)$p['title']).'</title>'.PHP_EOL;
186
            $xml .= '            <userid>'.$pageUserId.'</userid>'.PHP_EOL; // <-- new: page-level userid
187
            $xml .= '            <cachedcontent><![CDATA['.$pageHtml.']]></cachedcontent>'.PHP_EOL; // <-- not NULL
188
            $xml .= '            <timecreated>'.(int)$p['timecreated'].'</timecreated>'.PHP_EOL;
189
            $xml .= '            <timemodified>'.(int)$p['timemodified'].'</timemodified>'.PHP_EOL;
190
            $xml .= '            <firstversionid>'.$pid.'</firstversionid>'.PHP_EOL;
191
192
            // one version
193
            $xml .= '            <versions>'.PHP_EOL;
194
            $xml .= '              <version id="'.$pid.'">'.PHP_EOL;
195
            $xml .= '                <content><![CDATA['.$pageHtml.']]></content>'.PHP_EOL;
196
            $xml .= '                <contentformat>'.$this->h((string)$p['contentformat']).'</contentformat>'.PHP_EOL;
197
            $xml .= '                <version>'.(int)$p['version'].'</version>'.PHP_EOL;
198
            $xml .= '                <timecreated>'.(int)$p['timecreated'].'</timecreated>'.PHP_EOL;
199
            $xml .= '                <userid>'.$pageUserId.'</userid>'.PHP_EOL;
200
            $xml .= '              </version>'.PHP_EOL;
201
            $xml .= '            </versions>'.PHP_EOL;
202
203
            $xml .= '          </page>'.PHP_EOL;
204
        }
205
        $xml .= '        </pages>'.PHP_EOL;
206
207
        $xml .= '      </subwiki>'.PHP_EOL;
208
        $xml .= '    </subwikis>'.PHP_EOL;
209
210
        $xml .= '  </wiki>'.PHP_EOL;
211
        $xml .= '</activity>';
212
213
        $this->createXmlFile('wiki', $xml, $dir);
214
    }
215
216
    /** Normalize HTML like LabelExport: rewrite /document/... to @@PLUGINFILE@@/<file>. */
217
    private function normalizeContent(string $html): string
218
    {
219
        if ($html === '') {
220
            return $html;
221
        }
222
223
        // srcset
224
        $html = (string)preg_replace_callback(
225
            '~\bsrcset\s*=\s*([\'"])(.*?)\1~is',
226
            function (array $m): string {
227
                $q = $m[1]; $val = $m[2];
228
                $parts = array_map('trim', explode(',', $val));
229
                foreach ($parts as &$p) {
230
                    if ($p === '') { continue; }
231
                    $tokens = preg_split('/\s+/', $p, -1, PREG_SPLIT_NO_EMPTY);
232
                    if (!$tokens) { continue; }
233
                    $url = $tokens[0];
234
                    $new = $this->rewriteDocUrl($url);
235
                    if ($new !== $url) {
236
                        $tokens[0] = $new;
237
                        $p = implode(' ', $tokens);
238
                    }
239
                }
240
                return 'srcset='.$q.implode(', ', $parts).$q;
241
            },
242
            $html
243
        );
244
245
        // generic attributes
246
        $html = (string)preg_replace_callback(
247
            '~\b(src|href|poster|data)\s*=\s*([\'"])([^\'"]+)\2~i',
248
            fn(array $m) => $m[1].'='.$m[2].$this->rewriteDocUrl($m[3]).$m[2],
249
            $html
250
        );
251
252
        // inline CSS
253
        $html = (string)preg_replace_callback(
254
            '~\bstyle\s*=\s*([\'"])(.*?)\1~is',
255
            function (array $m): string {
256
                $q = $m[1]; $style = $m[2];
257
                $style = (string)preg_replace_callback(
258
                    '~url\((["\']?)([^)\'"]+)\1\)~i',
259
                    fn(array $mm) => 'url('.$mm[1].$this->rewriteDocUrl($mm[2]).$mm[1].')',
260
                    $style
261
                );
262
                return 'style='.$q.$style.$q;
263
            },
264
            $html
265
        );
266
267
        // <style> blocks
268
        $html = (string)preg_replace_callback(
269
            '~(<style\b[^>]*>)(.*?)(</style>)~is',
270
            function (array $m): string {
271
                $open = $m[1]; $css = $m[2]; $close = $m[3];
272
                $css = (string)preg_replace_callback(
273
                    '~url\((["\']?)([^)\'"]+)\1\)~i',
274
                    fn(array $mm) => 'url('.$mm[1].$this->rewriteDocUrl($mm[2]).$mm[1].')',
275
                    $css
276
                );
277
                return $open.$css.$close;
278
            },
279
            $html
280
        );
281
282
        return $html;
283
    }
284
285
    /** Replace Chamilo /document URLs by @@PLUGINFILE@@/basename */
286
    private function rewriteDocUrl(string $url): string
287
    {
288
        if ($url === '' || str_contains($url, '@@PLUGINFILE@@')) {
289
            return $url;
290
        }
291
        if (preg_match('#/(?:courses/[^/]+/)?document(/[^?\'" )]+)#i', $url, $m)) {
292
            return '@@PLUGINFILE@@/'.basename($m[1]);
293
        }
294
        return $url;
295
    }
296
297
    private function toTimestamp(string $value, int $fallback): int
298
    {
299
        if ($value === '') { return $fallback; }
300
        if (\is_numeric($value)) { return (int)$value; }
301
        $t = strtotime($value);
302
        return false !== $t ? (int)$t : $fallback;
303
    }
304
305
    private function h(string $s): string
306
    {
307
        return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
308
    }
309
}
310