Passed
Pull Request — master (#6921)
by
unknown
09:03
created

CcBase::countInstances()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 10
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 19
rs 8.8333
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\Base;
8
9
use DOMDocument;
10
use DOMXPath;
11
use Exception;
12
use RecursiveDirectoryIterator;
13
use RecursiveIteratorIterator;
14
15
use const DIRECTORY_SEPARATOR;
16
use const LIBXML_NONET;
17
18
class CcBase
19
{
20
    /**
21
     * Common Cartridge v1.3 resource types (strings as they appear in manifest).
22
     */
23
    public const CC_TYPE_FORUM = 'imsdt_xmlv1p3';
24
    public const CC_TYPE_QUIZ = 'imsqti_xmlv1p3/imscc_xmlv1p3/assessment';
25
    public const CC_TYPE_QUESTION_BANK = 'imsqti_xmlv1p3/imscc_xmlv1p3/question-bank';
26
    public const CC_TYPE_WEBLINK = 'imswl_xmlv1p3';
27
    public const CC_TYPE_WEBCONTENT = 'webcontent';
28
    public const CC_TYPE_ASSOCIATED_CONTENT = 'associatedcontent/imscc_xmlv1p3/learning-application-resource';
29
    public const CC_TYPE_EMPTY = '';
30
31
    /**
32
     * Internal tool types (used as keys within $instances). Keep them stable.
33
     */
34
    public const TOOL_TYPE_FORUM = 'forum';
35
    public const TOOL_TYPE_QUIZ = 'quiz';
36
    public const TOOL_TYPE_WEBLINK = 'weblink';
37
    public const TOOL_TYPE_DOCUMENT = 'document';
38
    public const TOOL_TYPE_UNKNOWN = 'unknown';
39
40
    /**
41
     * Depth constant for top-level items inside <organization>.
42
     */
43
    public const ROOT_DEEP = 1;
44
45
    /**
46
     * Legacy/rest helpers (kept for compatibility).
47
     */
48
    public static $restypes = ['associatedcontent/imscc_xmlv1p0/learning-application-resource', 'webcontent'];
49
    public static $forumns = ['dt' => 'http://www.imsglobal.org/xsd/imsdt_v1p0'];
50
    public static $quizns = ['xmlns' => 'http://www.imsglobal.org/xsd/ims_qtiasiv1p2'];
51
    public static $resourcens = ['wl' => 'http://www.imsglobal.org/xsd/imswl_v1p0'];
52
53
    /**
54
     * Instances index and manifest handling.
55
     */
56
    public static $instances = [];
57
    public static $manifest;
58
    public static $pathToManifestFolder;
59
60
    /**
61
     * Default namespaces (older baseline). NOTE: Cc1p3Convert overrides these with v1p3 values.
62
     * Late static binding ensures subclass mappings are used when called from the subclass.
63
     */
64
    public static $namespaces = [
65
        'imscc' => 'http://www.imsglobal.org/xsd/imscc/imscp_v1p1',
66
        'lomimscc' => 'http://ltsc.ieee.org/xsd/imscc/LOM',
67
        'lom' => 'http://ltsc.ieee.org/xsd/LOM',
68
        'voc' => 'http://ltsc.ieee.org/xsd/LOM/vocab',
69
        'xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
70
        'cc' => 'http://www.imsglobal.org/xsd/imsccauth_v1p0',
71
    ];
72
73
    public function __construct($path_to_manifest)
74
    {
75
        static::$manifest = new DOMDocument();
76
        static::$manifest->validateOnParse = false;
77
78
        static::$pathToManifestFolder = \dirname($path_to_manifest);
79
80
        static::logAction('Process start');
81
        static::logAction('Load the manifest file: '.$path_to_manifest);
82
83
        if (!static::$manifest->load($path_to_manifest, LIBXML_NONET)) {
84
            static::logAction('Cannot load the manifest file: '.$path_to_manifest, true);
85
        }
86
    }
87
88
    /**
89
     * @return array
90
     */
91
    public static function getquizns()
92
    {
93
        return static::$quizns;
94
    }
95
96
    /**
97
     * @return array
98
     */
99
    public static function getforumns()
100
    {
101
        return static::$forumns;
102
    }
103
104
    /**
105
     * @return array
106
     */
107
    public static function getresourcens()
108
    {
109
        return static::$resourcens;
110
    }
111
112
    /**
113
     * Find the imsmanifest.xml file inside the given folder and return its path.
114
     *
115
     * @param string $folder Full path name of the folder in which we expect to find imsmanifest.xml
116
     *
117
     * @return false|string
118
     */
119
    public static function getManifest(string $folder)
120
    {
121
        if (!is_dir($folder)) {
122
            return false;
123
        }
124
125
        // Quick top-level check
126
        if (file_exists($folder.'/imsmanifest.xml')) {
127
            return $folder.'/imsmanifest.xml';
128
        }
129
130
        $result = false;
131
132
        try {
133
            $dirIter = new RecursiveDirectoryIterator($folder, RecursiveDirectoryIterator::KEY_AS_PATHNAME);
134
            $recIter = new RecursiveIteratorIterator($dirIter, RecursiveIteratorIterator::CHILD_FIRST);
135
            foreach ($recIter as $info) {
136
                if ($info->isFile() && ('imsmanifest.xml' === $info->getFilename())) {
137
                    $result = $info->getPathname();
138
139
                    break;
140
                }
141
            }
142
        } catch (Exception $e) {
143
            // Non-fatal: just skip and return false
144
            static::logAction('Warning: Exception while scanning for imsmanifest.xml: '.$e->getMessage());
145
        }
146
147
        return $result;
148
    }
149
150
    public function isAuth()
151
    {
152
        $xpath = static::newxPath(static::$manifest, static::$namespaces);
153
        $count_auth = $xpath->evaluate('count(/imscc:manifest/cc:authorizations)');
154
155
        return $count_auth > 0;
156
    }
157
158
    public function getNodesByCriteria($key, $value)
159
    {
160
        $response = [];
161
162
        if (\array_key_exists('index', static::$instances)) {
163
            foreach (static::$instances['index'] as $item) {
164
                if ($item[$key] == $value) {
165
                    $response[] = $item;
166
                }
167
            }
168
        }
169
170
        return $response;
171
    }
172
173
    public function countInstances($type)
174
    {
175
        $quantity = 0;
176
177
        if (\array_key_exists('index', static::$instances)) {
178
            if (!empty(static::$instances['index']) && $type) {
179
                $types = []; // Initialize accumulator to avoid notices
180
                foreach (static::$instances['index'] as $instance) {
181
                    if (!empty($instance['tool_type'])) {
182
                        $types[] = $instance['tool_type'];
183
                    }
184
                }
185
186
                $quantityInstances = array_count_values($types);
187
                $quantity = \array_key_exists($type, $quantityInstances) ? $quantityInstances[$type] : 0;
188
            }
189
        }
190
191
        return $quantity;
192
    }
193
194
    public function getItemCcType($identifier)
195
    {
196
        $xpath = static::newxPath(static::$manifest, static::$namespaces);
197
        $nodes = $xpath->query('/imscc:manifest/imscc:resources/imscc:resource[@identifier="'.$identifier.'"]/@type');
198
199
        if ($nodes && !empty($nodes->item(0)->nodeValue)) {
200
            return $nodes->item(0)->nodeValue;
201
        }
202
203
        return '';
204
    }
205
206
    public function getItemHref($identifier)
207
    {
208
        $xpath = static::newxPath(static::$manifest, static::$namespaces);
209
        $nodes = $xpath->query('/imscc:manifest/imscc:resources/imscc:resource[@identifier="'.$identifier.'"]/imscc:file/@href');
210
211
        if ($nodes && !empty($nodes->item(0)->nodeValue)) {
212
            return $nodes->item(0)->nodeValue;
213
        }
214
215
        return '';
216
    }
217
218
    public static function newxPath(DOMDocument $manifest, $namespaces = '')
219
    {
220
        $xpath = new DOMXPath($manifest);
221
222
        if (!empty($namespaces)) {
223
            foreach ($namespaces as $prefix => $ns) {
224
                if (!$xpath->registerNamespace($prefix, $ns)) {
225
                    // Critical because namespace mismatches will break all queries
226
                    static::logAction('Cannot register the namespace: '.$prefix.':'.$ns, true);
227
                }
228
            }
229
        }
230
231
        return $xpath;
232
    }
233
234
    public static function logFile()
235
    {
236
        return static::$pathToManifestFolder.DIRECTORY_SEPARATOR.'cc_import.log';
237
    }
238
239
    /**
240
     * Simple, file-based logging. Critical errors abort the process.
241
     *
242
     * @param null|mixed $context
243
     */
244
    public static function logAction(string $message, $context = null, bool $ok = true): void
245
    {
246
        // Minimal, centralized logger for CC 1.3 importer steps
247
        error_log('(imscc13) '.$message.' , level: '.($ok ? 'info' : 'warn').' , extra: '.json_encode($context));
248
    }
249
250
    /**
251
     * Map CC resource type (as read from manifest) to an internal tool type string.
252
     * Always returns a defined TOOL_TYPE_* constant.
253
     *
254
     * @param mixed $ccType
255
     */
256
    public function convertToToolType($ccType)
257
    {
258
        $type = self::TOOL_TYPE_UNKNOWN;
259
260
        if ($ccType === static::CC_TYPE_FORUM) {
261
            $type = self::TOOL_TYPE_FORUM;
262
        } elseif ($ccType === static::CC_TYPE_QUIZ) {
263
            $type = self::TOOL_TYPE_QUIZ;
264
        } elseif ($ccType === static::CC_TYPE_WEBLINK) {
265
            $type = self::TOOL_TYPE_WEBLINK;
266
        } elseif ($ccType === static::CC_TYPE_WEBCONTENT) {
267
            $type = self::TOOL_TYPE_DOCUMENT;
268
        }
269
270
        return $type;
271
    }
272
273
    protected function getMetadata($section, $key)
274
    {
275
        $xpath = static::newxPath(static::$manifest, static::$namespaces);
276
        $metadata = $xpath->query('/imscc:manifest/imscc:metadata/lomimscc:lom/lomimscc:'.$section.'/lomimscc:'.$key.'/lomimscc:string');
277
        $value = !empty($metadata->item(0)->nodeValue) ? $metadata->item(0)->nodeValue : '';
278
279
        return $value;
280
    }
281
282
    /**
283
     * Is activity visible or not (based on LOM roles metadata).
284
     *
285
     * @param string $identifier
286
     *
287
     * @return int 1 visible, 0 hidden
288
     */
289
    protected function getModuleVisible($identifier)
290
    {
291
        $mod_visible = 1;
292
        if (!empty($identifier)) {
293
            $xpath = static::newxPath(static::$manifest, static::$namespaces);
294
            $query = '/imscc:manifest/imscc:resources/imscc:resource[@identifier="'.$identifier.'"]';
295
            $query .= '//lom:intendedEndUserRole/voc:vocabulary/lom:value';
296
            $intendeduserrole = $xpath->query($query);
297
            if (!empty($intendeduserrole) && ($intendeduserrole->length > 0)) {
298
                $role = trim($intendeduserrole->item(0)->nodeValue);
299
                if (0 === strcasecmp('Instructor', $role)) {
300
                    $mod_visible = 0;
301
                }
302
            }
303
        }
304
305
        return $mod_visible;
306
    }
307
308
    /**
309
     * Build the internal flat index of resources/items with hierarchy metadata.
310
     * Adds robust logging to help troubleshooting unknown/missing types/titles.
311
     *
312
     * @param mixed $items
313
     * @param mixed $level
314
     * @param mixed $array_index
315
     * @param mixed $index_root
316
     */
317
    protected function createInstances($items, $level = 0, &$array_index = 0, $index_root = 0): void
318
    {
319
        $level++;
320
        $i = 1;
321
322
        if ($items) {
323
            $xpath = self::newxPath(static::$manifest, static::$namespaces);
324
325
            foreach ($items as $item) {
326
                $array_index++;
327
                $title = $path = $tool_type = $identifierref = '';
328
                $ccType = '';
329
330
                if ('item' === $item->nodeName) {
331
                    if ($item->hasAttribute('identifierref')) {
332
                        $identifierref = $item->getAttribute('identifierref');
333
                    }
334
335
                    $titles = $xpath->query('imscc:title', $item);
336
                    if ($titles->length > 0) {
337
                        $title = $titles->item(0)->nodeValue ?? '';
338
                    }
339
340
                    $ccType = $this->getItemCcType($identifierref);
341
                    $tool_type = $this->convertToToolType($ccType);
342
343
                    // If completely empty (label-only folder), keep as unknown
344
                    if (empty($identifierref) && empty($title)) {
345
                        $tool_type = self::TOOL_TYPE_UNKNOWN;
346
                    }
347
                } elseif ('resource' === $item->nodeName) {
348
                    $identifierref = $xpath->query('@identifier', $item);
349
                    $identifierref = !empty($identifierref->item(0)->nodeValue) ? $identifierref->item(0)->nodeValue : '';
350
351
                    $ccType = $this->getItemCcType($identifierref);
352
                    $tool_type = $this->convertToToolType($ccType);
353
354
                    if (self::CC_TYPE_WEBCONTENT === $ccType) {
355
                        $path = $this->getItemHref($identifierref);
356
                        $title = basename((string) $path);
357
                    } else {
358
                        // For non-file resources (e.g., question bank), give a stable label
359
                        $title = 'Quiz Bank '.($this->countInstances($tool_type) + 1);
360
                    }
361
                }
362
363
                if (self::ROOT_DEEP === $level) {
364
                    $index_root = $array_index;
365
                }
366
367
                // Log each discovered entry
368
                static::logAction(\sprintf(
369
                    'Indexing node: nodeName=%s, identifier=%s, ccType=%s, toolType=%s, title=%s, level=%d',
370
                    $item->nodeName,
371
                    $identifierref ?: '(none)',
372
                    $ccType ?: '(none)',
373
                    $tool_type ?: '(none)',
374
                    $title ?: '(empty)',
375
                    $level
376
                ));
377
378
                static::$instances['index'][$array_index] = [
379
                    'common_cartridge_type' => $ccType,
380
                    'tool_type' => $tool_type,
381
                    'title' => $title ?: '',
382
                    'root_parent' => $index_root,
383
                    'index' => $array_index,
384
                    'deep' => $level,
385
                    'instance' => $this->countInstances($tool_type),
386
                    'resource_identifier' => $identifierref,
387
                ];
388
389
                static::$instances['instances'][$tool_type][] = [
390
                    'title' => $title,
391
                    'instance' => static::$instances['index'][$array_index]['instance'],
392
                    'common_cartridge_type' => $ccType,
393
                    'resource_identifier' => $identifierref,
394
                    'deep' => $level,
395
                    'src' => $path,
396
                ];
397
398
                $more_items = $xpath->query('imscc:item', $item);
399
                if ($more_items->length > 0) {
400
                    $this->createInstances($more_items, $level, $array_index, $index_root);
401
                }
402
403
                $i++;
404
            }
405
        }
406
    }
407
408
    protected static function criticalError($text): void
409
    {
410
        $path_to_log = static::logFile();
411
412
        echo '
413
        <p>
414
        <hr />A critical error has been found!
415
        <p>'.htmlentities($text).'</p>
416
        <p>
417
        The process has been stopped. Please see the <a href="'.htmlentities($path_to_log).'">log file</a> for more information.</p>
418
        <p>Log: '.htmlentities($path_to_log).'</p>
419
        <hr />
420
        </p>
421
        ';
422
423
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
424
    }
425
426
    protected function createCourseCode($title)
427
    {
428
        // Ensure shortname does not exceed DB limit and leave room for platform suffixes
429
        return substr(strtoupper(str_replace(' ', '', trim((string) $title))), 0, 94);
430
    }
431
}
432