Passed
Push — master ( 915f8b...f333c5 )
by
unknown
10:12
created

Cc13Resource   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 408
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 234
c 1
b 0
f 0
dl 0
loc 408
rs 2.64
wmc 72

5 Methods

Rating   Name   Duplication   Size   Complexity  
F storeDocuments() 0 188 28
C validateUrlSyntax() 0 35 12
F getResourceData() 0 93 22
B storeLinks() 0 47 7
A generateData() 0 10 3

How to fix   Complexity   

Complex Class

Complex classes like Cc13Resource often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Cc13Resource, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CourseBundle\Component\CourseCopy\CommonCartridge\Import\Converter;
8
9
use Chamilo\CoreBundle\Framework\Container;
10
use Chamilo\CourseBundle\Component\CourseCopy\CommonCartridge\Import\Cc1p3Convert;
11
use Chamilo\CourseBundle\Entity\CLink;
12
use Database;
13
use DocumentManager;
14
use DOMXPath;
15
use Throwable;
16
17
use const DIRECTORY_SEPARATOR;
18
use const ENT_COMPAT;
19
use const ENT_QUOTES;
20
use const FILTER_VALIDATE_URL;
21
use const PATHINFO_EXTENSION;
22
use const PHP_URL_SCHEME;
23
24
class Cc13Resource extends Cc13Entities
25
{
26
    public function generateData($resource_type)
27
    {
28
        $data = [];
29
        if (!empty(Cc1p3Convert::$instances['instances'][$resource_type])) {
30
            foreach (Cc1p3Convert::$instances['instances'][$resource_type] as $instance) {
31
                $data[] = $this->getResourceData($instance);
32
            }
33
        }
34
35
        return $data;
36
    }
37
38
    /**
39
     * Store web links using Doctrine entities (Chamilo 2 style).
40
     *
41
     * @param mixed $links
42
     */
43
    public function storeLinks($links): bool
44
    {
45
        if (empty($links)) {
46
            return true;
47
        }
48
49
        $em = Database::getManager();
50
        $course = api_get_course_entity(api_get_course_int_id());
51
        $session = api_get_session_entity((int) api_get_session_id());
52
53
        foreach ($links as $link) {
54
            $title = trim(htmlspecialchars_decode((string) ($link[1] ?? ''), ENT_QUOTES)) ?: (string) ($link[4] ?? '');
55
            $url = trim((string) ($link[4] ?? ''));
56
            if ('' === $url) {
57
                Cc1p3Convert::logAction('storeLinks: empty URL skipped', ['title' => $title]);
58
59
                continue;
60
            }
61
62
            // Basic sanity check (best-effort).
63
            if (!self::validateUrlSyntax($url, 's+')) {
64
                $try = rawurldecode($url);
65
                if (self::validateUrlSyntax($try, 's+')) {
66
                    $url = $try;
67
                } else {
68
                    Cc1p3Convert::logAction('storeLinks: invalid URL skipped', ['url' => $url]);
69
70
                    continue; // Skip invalid URL instead of creating a broken entity.
71
                }
72
            }
73
74
            Cc1p3Convert::logAction('storeLinks: creating link', ['title' => $title, 'url' => $url]);
75
76
            $entity = (new CLink())
77
                ->setUrl($url)
78
                ->setTitle($title)
79
                ->setDescription('')
80
                ->setTarget('_blank')
81
                ->setParent($course)
82
                ->addCourseLink($course, $session)
83
            ;
84
85
            $em->persist($entity);
86
        }
87
        $em->flush();
88
89
        return true;
90
    }
91
92
    /**
93
     * Store Documents from CC package. No SYS_COURSE_PATH. Uses DocumentManager + ResourceFile.
94
     *
95
     * @param array  $documents   Items from getResourceData()
96
     * @param string $packageRoot Absolute path to the extracted package (directory of imsmanifest.xml)
97
     */
98
    public function storeDocuments(array $documents, string $packageRoot): bool
99
    {
100
        $packageRoot = rtrim($packageRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
101
102
        $courseInfo = api_get_course_info();
103
        $courseEntity = api_get_course_entity($courseInfo['real_id']);
104
        $sessionEnt = api_get_session_entity((int) api_get_session_id());
105
        $sessionId = (int) ($sessionEnt ? $sessionEnt->getId() : 0);
106
        $group = api_get_group_entity(0);
107
108
        $docRepo = Container::getDocumentRepository();
109
110
        // Ensure nested folder chain under Documents and return parent iid.
111
        $ensureFolder = function (string $relPath) use ($docRepo, $courseEntity, $courseInfo, $sessionEnt, $group, $sessionId): int {
112
            $rel = '/'.ltrim($relPath, '/');
113
            if ('/' === $rel || '' === $rel) {
114
                return 0;
115
            }
116
117
            $parts = array_values(array_filter(explode('/', trim($rel, '/'))));
118
            $accum = '';
119
            $parentId = 0;
120
121
            foreach ($parts as $seg) {
122
                $accum .= '/'.$seg;
123
124
                $parentRes = $parentId ? $docRepo->find($parentId) : $courseEntity;
125
126
                $existing = $docRepo->findCourseResourceByTitle(
127
                    $seg,
128
                    $parentRes->getResourceNode(),
0 ignored issues
show
Bug introduced by
The method getResourceNode() does not exist on null. ( Ignorable by Annotation )

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

128
                    $parentRes->/** @scrutinizer ignore-call */ 
129
                                getResourceNode(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
129
                    $courseEntity,
130
                    $sessionEnt,
131
                    $group
132
                );
133
134
                if ($existing && method_exists($existing, 'getIid')) {
135
                    $parentId = (int) $existing->getIid();
136
137
                    continue;
138
                }
139
140
                $entity = DocumentManager::addDocument(
141
                    ['real_id' => $courseInfo['real_id'], 'code' => $courseInfo['code']],
142
                    $accum,
143
                    'folder',
144
                    0,
145
                    $seg,
146
                    null,
147
                    0,
148
                    null,
149
                    0,
150
                    $sessionId,
151
                    0,
152
                    false,
153
                    '',
154
                    $parentId,
155
                    ''
156
                );
157
158
                $parentId = method_exists($entity, 'getIid') ? (int) $entity->getIid() : 0;
159
            }
160
161
            return $parentId;
162
        };
163
164
        // Base destination root for the package contents.
165
        $baseRel = '/commoncartridge';
166
        $ensureFolder($baseRel);
167
168
        foreach ($documents as $doc) {
169
            $type = (string) ($doc[2] ?? '');
170
171
            // Compute destination subfolder:
172
            $subdir = '';
173
            if ('file' === $type) {
174
                $ref = trim((string) ($doc[4] ?? ''));
175
                if ('' === $ref) {
176
                    continue;
177
                }
178
                $subdir = trim(\dirname($ref), '.\/');
179
            } elseif ('html' === $type) {
180
                $subdir = trim((string) ($doc[6] ?? ''), '/');
181
            } else {
182
                // Unknown type; skip gracefully.
183
                Cc1p3Convert::logAction('storeDocuments: unknown type skipped', ['type' => $type]);
184
185
                continue;
186
            }
187
188
            $destPath = $baseRel.($subdir ? '/'.$subdir : '');
189
            $parentId = $ensureFolder($destPath);
190
191
            // Collision-safe final title.
192
            $title = (string) ($doc[1] ?? '');
193
            $guessName = 'file' === $type ? basename((string) ($doc[4] ?? 'file.bin')) : 'page.html';
194
            $finalTitle = '' !== $title ? $title : $guessName;
195
196
            $parentRes = $parentId ? $docRepo->find($parentId) : $courseEntity;
197
            $nameExists = function (string $t) use ($docRepo, $parentRes, $courseEntity, $sessionEnt, $group): bool {
198
                $e = $docRepo->findCourseResourceByTitle(
199
                    $t,
200
                    $parentRes->getResourceNode(),
201
                    $courseEntity,
202
                    $sessionEnt,
203
                    $group
204
                );
205
206
                return (bool) ($e && method_exists($e, 'getIid'));
207
            };
208
209
            if ($nameExists($finalTitle)) {
210
                $pi = pathinfo($finalTitle);
211
                $base = $pi['filename'] ?? $finalTitle;
212
                $ext = isset($pi['extension']) && '' !== $pi['extension'] ? '.'.$pi['extension'] : '';
213
                $i = 1;
214
                while ($nameExists($base.'_'.$i.$ext)) {
215
                    $i++;
216
                }
217
                $finalTitle = $base.'_'.$i.$ext;
218
            }
219
220
            // Persist
221
            if ('file' === $type) {
222
                $ref = trim((string) ($doc[4] ?? ''));
223
                $src = $packageRoot.str_replace('\\', '/', $ref);
224
                if (!is_file($src) || !is_readable($src)) {
225
                    Cc1p3Convert::logAction('CC import: missing/unreadable file', ['src' => $src]);
226
227
                    continue;
228
                }
229
230
                try {
231
                    DocumentManager::addDocument(
232
                        ['real_id' => $courseInfo['real_id'], 'code' => $courseInfo['code']],
233
                        $destPath.'/'.$finalTitle,
234
                        'file',
235
                        (int) @filesize($src),
236
                        $finalTitle,
237
                        '',
238
                        0,
239
                        null,
240
                        0,
241
                        $sessionId,
242
                        0,
243
                        false,
244
                        '',      // no inline content for binaries
245
                        $parentId,
246
                        $src     // realPath → copied into ResourceFile storage
247
                    );
248
                } catch (Throwable $e) {
249
                    Cc1p3Convert::logAction('CC import: addDocument(file) failed', ['src' => $src, 'error' => $e->getMessage()]);
250
                }
251
252
                continue;
253
            }
254
255
            if ('html' === $type) {
256
                // Inline HTML content, keep relative links intact (folder layout is mirrored).
257
                $content = (string) ($doc[3] ?? '');
258
259
                try {
260
                    DocumentManager::addDocument(
261
                        ['real_id' => $courseInfo['real_id'], 'code' => $courseInfo['code']],
262
                        $destPath.'/'.$finalTitle,
263
                        'file',
264
                        (int) \strlen($content),
265
                        $finalTitle,
266
                        '',
267
                        0,
268
                        null,
269
                        0,
270
                        $sessionId,
271
                        0,
272
                        false,
273
                        $content, // inline HTML
274
                        $parentId,
275
                        ''        // no realPath when content provided
276
                    );
277
                } catch (Throwable $e) {
278
                    Cc1p3Convert::logAction('CC import: addDocument(html) failed', ['dest' => $destPath.'/'.$finalTitle, 'error' => $e->getMessage()]);
279
                }
280
281
                continue;
282
            }
283
        }
284
285
        return true;
286
    }
287
288
    /**
289
     * Build normalized resource tuple for the importer pipeline.
290
     * Returns:
291
     *  [0]=instance, [1]=title, [2]=type('file'|'html'), [3]=html, [4]=href, [5]=options, [6]=baseDir(html only).
292
     *
293
     * @param mixed $instance
294
     */
295
    public function getResourceData($instance)
296
    {
297
        $xpath = Cc1p3Convert::newxPath(Cc1p3Convert::$manifest, Cc1p3Convert::$namespaces);
298
        $link = '';
299
        $baseDir = '';
300
301
        if (
302
            Cc1p3Convert::CC_TYPE_WEBCONTENT == $instance['common_cartridge_type']
303
            || Cc1p3Convert::CC_TYPE_ASSOCIATED_CONTENT == $instance['common_cartridge_type']
304
        ) {
305
            $resource = $xpath->query(
306
                '/imscc:manifest/imscc:resources/imscc:resource[@identifier="'.$instance['resource_identifier'].'"]/@href'
307
            );
308
            if ($resource->length > 0) {
309
                $resource = !empty($resource->item(0)->nodeValue) ? $resource->item(0)->nodeValue : '';
310
            } else {
311
                $resource = '';
312
            }
313
314
            if (empty($resource)) {
315
                // Fallback: use src set in CcBase::createInstances() from <file href="...">
316
                $resource = $instance['src'];
317
            }
318
            if (!empty($resource)) {
319
                $link = $resource;
320
                $baseDir = trim(\dirname($resource), '.\/');
321
            }
322
        }
323
324
        if (Cc1p3Convert::CC_TYPE_WEBLINK == $instance['common_cartridge_type']) {
325
            $external_resource = $instance['src'];
326
            if (!empty($external_resource)) {
327
                $resourceDoc = $this->loadXmlResource(
328
                    Cc1p3Convert::$pathToManifestFolder.DIRECTORY_SEPARATOR.$external_resource
329
                );
330
331
                if (!empty($resourceDoc)) {
332
                    // Namespace-agnostic: webLink/url with any NS
333
                    $x = new DOMXPath($resourceDoc);
334
                    $href = $x->query('/*[local-name()="webLink"]/*[local-name()="url"]/@href');
335
                    if ($href->length > 0) {
336
                        $raw = trim((string) $href->item(0)->nodeValue);
337
                        Cc1p3Convert::logAction('getResourceData: webLink extracted', ['raw' => $raw]);
338
339
                        if (!self::validateUrlSyntax($raw, 's+')) {
340
                            $changed = rawurldecode($raw);
341
                            if (self::validateUrlSyntax($changed, 's+')) {
342
                                $link = $changed;
343
                            } else {
344
                                Cc1p3Convert::logAction('getResourceData: invalid webLink URL', ['raw' => $raw]);
345
                                $link = 'http://invalidurldetected/';
346
                            }
347
                        } else {
348
                            $link = htmlspecialchars($raw, ENT_COMPAT, 'UTF-8', false);
349
                        }
350
                    } else {
351
                        Cc1p3Convert::logAction('getResourceData: webLink href not found via XPath(local-name())', ['file' => $external_resource]);
352
                    }
353
                }
354
            }
355
        }
356
357
        // Decide type: file vs html
358
        $type = 'file';
359
        $htmlContent = '';
360
        $options = '';
361
362
        if (!empty($link) && (Cc1p3Convert::CC_TYPE_WEBCONTENT == $instance['common_cartridge_type'])) {
363
            $ext = strtolower(pathinfo($link, PATHINFO_EXTENSION) ?: '');
364
            if (\in_array($ext, ['html', 'htm', 'xhtml'], true)) {
365
                $type = 'html';
366
367
                $root = realpath(Cc1p3Convert::$pathToManifestFolder);
368
                $abs = $root ? realpath($root.DIRECTORY_SEPARATOR.$link) : false;
369
370
                if ($abs && is_file($abs)) {
371
                    // Read HTML and strip outer wrappers; keep relative URLs as-is.
372
                    $raw = (string) @file_get_contents($abs);
373
                    $htmlContent = self::safexml($this->prepareContent($raw));
374
                    // For inline HTML we clear the href; the storage path is decided in storeDocuments.
375
                    $link = '';
376
                }
377
            }
378
        }
379
380
        return [
381
            $instance['instance'],
382
            self::safexml($instance['title'] ?: ($link ? basename($link) : '')),
383
            $type,
384
            $htmlContent,
385
            $link,
386
            $options,
387
            $baseDir,
388
        ];
389
    }
390
391
    /**
392
     * Simple URL validator.
393
     * $mode:
394
     *  - 's+' => require scheme (http/https) and validate full URL
395
     *  - 's*' => scheme optional (rarely used here).
396
     */
397
    private static function validateUrlSyntax(string $url, string $mode = 's+'): bool
398
    {
399
        $u = trim($url);
400
        if ('' === $u) {
401
            return false;
402
        }
403
404
        // Try original first
405
        if (false !== filter_var($u, FILTER_VALIDATE_URL)) {
406
            $scheme = strtolower(parse_url($u, PHP_URL_SCHEME) ?? '');
407
            if ('' === $scheme && 's+' === $mode) {
408
                return false;
409
            }
410
            if ('' !== $scheme && !\in_array($scheme, ['http', 'https'], true)) {
411
                return false;
412
            }
413
414
            return true;
415
        }
416
417
        // Try decoded form (handles %20 etc.)
418
        $dec = rawurldecode($u);
419
        if (false !== filter_var($dec, FILTER_VALIDATE_URL)) {
420
            $scheme = strtolower(parse_url($dec, PHP_URL_SCHEME) ?? '');
421
            if ('' === $scheme && 's+' === $mode) {
422
                return false;
423
            }
424
            if ('' !== $scheme && !\in_array($scheme, ['http', 'https'], true)) {
425
                return false;
426
            }
427
428
            return true;
429
        }
430
431
        return false;
432
    }
433
}
434