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

CcVersion13::createOrganizationNode()   B

Complexity

Conditions 7
Paths 18

Size

Total Lines 30
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 16
c 1
b 0
f 0
nc 18
nop 3
dl 0
loc 30
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\Export;
8
9
use Chamilo\CourseBundle\Component\CourseCopy\CommonCartridge\Export\Base\CcVersion1;
10
use Chamilo\CourseBundle\Component\CourseCopy\CommonCartridge\Export\Interfaces\CcIOrganization;
11
use Chamilo\CourseBundle\Component\CourseCopy\CommonCartridge\Export\Interfaces\CcIResource;
12
use DOMDocument;
13
use DOMElement;
14
use DOMNode;
15
use DOMText;
16
use DOMXPath;
17
18
/**
19
 * CC v1.3 implementation aligned with what CcManifest expects.
20
 * IMPORTANT: Method signatures MUST match CcVersionBase (references and optional args).
21
 */
22
class CcVersion13 extends CcVersion1
23
{
24
    public const WEBCONTENT = 'webcontent';
25
    public const QUESTIONBANK = 'imsqti_xmlv1p3/imscc_xmlv1p3/question-bank';
26
    public const ASSESSMENT = 'imsqti_xmlv1p3/imscc_xmlv1p3/assessment';
27
    public const ASSOCIATEDCONTENT = 'associatedcontent/imscc_xmlv1p3/learning-application-resource';
28
    public const DISCUSSIONTOPIC = 'imsdt_xmlv1p3';
29
    public const WEBLINK = 'imswl_xmlv1p3';
30
    public const BASICLTI = 'imsbasiclti_xmlv1p3';
31
32
    /**
33
     * @var string[]
34
     */
35
    public static $checker = [
36
        self::WEBCONTENT,
37
        self::ASSESSMENT,
38
        self::ASSOCIATEDCONTENT,
39
        self::DISCUSSIONTOPIC,
40
        self::QUESTIONBANK,
41
        self::WEBLINK,
42
        self::BASICLTI,
43
    ];
44
45
    private ?string $manifestId = null;
46
47
    /**
48
     * @var string
49
     */
50
    protected $base = '';
51
52
    public function __construct()
53
    {
54
        // CC 1.3 namespaces.
55
        $this->ccnamespaces = [
56
            'imscc' => 'http://www.imsglobal.org/xsd/imsccv1p3/imscp_v1p1',
57
            'lomimscc' => 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/manifest',
58
            'lom' => 'http://ltsc.ieee.org/xsd/imsccv1p3/LOM/resource',
59
            'xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
60
            'cc' => 'http://www.imsglobal.org/xsd/imsccv1p3/imsccauth_v1p1',
61
        ];
62
63
        // Optional schema locations.
64
        $this->ccnsnames = [
65
            'imscc' => 'http://www.imsglobal.org/profile/cc/ccv1p3/ccv1p3_imscp_v1p2_v1p0.xsd',
66
            'lomimscc' => 'http://www.imsglobal.org/profile/cc/ccv1p3/LOM/ccv1p3_lommanifest_v1p0.xsd',
67
            'lom' => 'http://www.imsglobal.org/profile/cc/ccv1p3/LOM/ccv1p3_lomresource_v1p0.xsd',
68
        ];
69
70
        $this->ccversion = '1.3.0';
71
        $this->camversion = '1.3.0';
72
        $this->_generator = 'Chamilo Common Cartridge generator';
73
74
        parent::__construct();
75
    }
76
77
    /**
78
     * Validate allowed resource types.
79
     */
80
    public function valid($type)
81
    {
82
        return \in_array($type, self::$checker, true);
83
    }
84
85
    /**
86
     * Expose namespaces for XPath registration.
87
     */
88
    public function getCcNamespaces(): array
89
    {
90
        return $this->ccnamespaces;
91
    }
92
93
    /**
94
     * Manifest identifier getter (lazy).
95
     */
96
    public function manifestID(): string
97
    {
98
        if (null === $this->manifestId) {
99
            $this->manifestId = 'MANIFEST-'.bin2hex(random_bytes(6));
100
        }
101
102
        return $this->manifestId;
103
    }
104
105
    /**
106
     * Base path used by the manifest (<resource base="...">). Empty by default.
107
     */
108
    public function base(): string
109
    {
110
        return $this->base;
111
    }
112
113
    /**
114
     * MUST match: CcVersionBase->createManifest(&doc: DOMDocument, [rootmanifestnode = null])
115
     * Creates the root <manifest> with <organizations>, <resources>, <metadata>.
116
     *
117
     * @param mixed $rootmanifestnode optional parent to append the manifest into
118
     *
119
     * @return mixed the created manifest element (kept loose to match base)
120
     */
121
    public function createManifest(DOMDocument &$doc, $rootmanifestnode = null)
122
    {
123
        $imscc = $this->ccnamespaces['imscc'];
124
125
        $manifest = $doc->createElementNS($imscc, 'manifest');
126
        $manifest->setAttribute('identifier', $this->manifestID());
127
128
        // Declare xmlns:prefix for all namespaces (helps validators).
129
        foreach ($this->ccnamespaces as $prefix => $uri) {
130
            $manifest->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:'.$prefix, $uri);
131
        }
132
133
        // Optional xsi:schemaLocation.
134
        if (isset($this->ccnamespaces['xsi']) && !empty($this->ccnsnames)) {
135
            $schemaLocation = '';
136
            foreach ($this->ccnsnames as $key => $value) {
137
                $schemaLocation .= ('' === $schemaLocation ? '' : ' ')
138
                    .$this->ccnamespaces[$key].' '.$value;
139
            }
140
            if ('' !== $schemaLocation) {
141
                $manifest->setAttributeNS(
142
                    $this->ccnamespaces['xsi'],
143
                    'xsi:schemaLocation',
144
                    $schemaLocation
145
                );
146
            }
147
        }
148
149
        // Standard children
150
        $organizations = $doc->createElementNS($imscc, 'organizations');
151
        $resources = $doc->createElementNS($imscc, 'resources');
152
        $metadata = $doc->createElementNS($imscc, 'metadata');
153
        $manifest->appendChild($organizations);
154
        $manifest->appendChild($resources);
155
        $manifest->appendChild($metadata);
156
157
        // Append to given parent or as document element.
158
        if ($rootmanifestnode instanceof DOMNode) {
159
            $rootmanifestnode->appendChild($manifest);
160
        } else {
161
            $doc->appendChild($manifest);
162
        }
163
164
        return $manifest;
165
    }
166
167
    /**
168
     * Create an <organization> node. Signature is additive to base style (keeps optional $xmlnode).
169
     *
170
     * @param null|mixed $xmlnode
171
     */
172
    public function createOrganizationNode(CcIOrganization &$org, DOMDocument &$doc, $xmlnode = null)
173
    {
174
        $imscc = $this->ccnamespaces['imscc'];
175
176
        $organization = $doc->createElementNS($imscc, 'organization');
177
        $organization->setAttribute('identifier', 'ORG-'.bin2hex(random_bytes(4)));
178
179
        // Optional title
180
        if (method_exists($org, 'getTitle')) {
181
            $title = (string) $org->getTitle();
182
            if ('' !== $title) {
183
                $titleNode = $doc->createElementNS($imscc, 'title');
184
                $titleNode->appendChild(new DOMText($title));
185
                $organization->appendChild($titleNode);
186
            }
187
        }
188
189
        // Optional items
190
        if (method_exists($org, 'getItems')) {
191
            $items = $org->getItems();
192
            if (\is_array($items) && !empty($items)) {
193
                $this->updateItems($items, $doc, $organization);
194
            }
195
        }
196
197
        if ($xmlnode instanceof DOMNode) {
198
            $xmlnode->appendChild($organization);
199
        }
200
201
        return $organization;
202
    }
203
204
    /**
205
     * MUST match: CcVersionBase->createResourceNode(&res: CcIResource, &doc: DOMDocument, [xmlnode = null])
206
     * Creates a <resource> element and appends it under $xmlnode if provided, or under //imscc:resources.
207
     *
208
     * @param mixed $xmlnode optional parent (usually the <resources> container)
209
     *
210
     * @return mixed the created resource element (kept loose to match base)
211
     */
212
    public function createResourceNode(CcIResource &$res, DOMDocument &$doc, $xmlnode = null)
213
    {
214
        $imscc = $this->ccnamespaces['imscc'];
215
216
        // Find container if none provided.
217
        if (!$xmlnode instanceof DOMNode) {
218
            $xpath = new DOMXPath($doc);
219
            foreach ($this->ccnamespaces as $p => $u) {
220
                $xpath->registerNamespace($p, $u);
221
            }
222
            $xmlnode = $xpath->query('//imscc:resources')->item(0);
223
            if (!$xmlnode instanceof DOMNode) {
224
                $xmlnode = $doc->documentElement; // Fallback
225
            }
226
        }
227
228
        // Normalize fields.
229
        $identifier = $res->identifier ?? ('RES-'.bin2hex(random_bytes(6)));
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Chamilo\CourseBundle\Com...\Interfaces\CcIResource suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
230
        $type = $res->type ?? self::WEBCONTENT;
0 ignored issues
show
Bug introduced by
Accessing type on the interface Chamilo\CourseBundle\Com...\Interfaces\CcIResource suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
231
        $href = $res->href ?? null;
0 ignored issues
show
Bug introduced by
Accessing href on the interface Chamilo\CourseBundle\Com...\Interfaces\CcIResource suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
232
        $base = $res->base ?? null;
0 ignored issues
show
Bug introduced by
Accessing base on the interface Chamilo\CourseBundle\Com...\Interfaces\CcIResource suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
233
234
        $resourceEl = $doc->createElementNS($imscc, 'resource');
235
        $resourceEl->setAttribute('identifier', $identifier);
236
        $resourceEl->setAttribute('type', $type);
237
        if (!empty($href)) {
238
            $resourceEl->setAttribute('href', (string) $href);
239
        }
240
        if (!empty($base)) {
241
            $resourceEl->setAttribute('base', (string) $base);
242
        }
243
244
        // Files
245
        $files = \is_array($res->files ?? null) ? $res->files : [];
0 ignored issues
show
Bug introduced by
Accessing files on the interface Chamilo\CourseBundle\Com...\Interfaces\CcIResource suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
246
        foreach ($files as $fileHref) {
247
            if (null === $fileHref || '' === $fileHref) {
248
                continue;
249
            }
250
            $fileEl = $doc->createElementNS($imscc, 'file');
251
            $fileEl->setAttribute('href', (string) $fileHref);
252
            $resourceEl->appendChild($fileEl);
253
254
            if (property_exists($this, 'resourcesInd')) {
255
                $this->resourcesInd[(string) $fileHref] = $identifier;
256
            }
257
        }
258
259
        // Dependencies
260
        $deps = \is_array($res->dependency ?? null) ? $res->dependency : [];
0 ignored issues
show
Bug introduced by
Accessing dependency on the interface Chamilo\CourseBundle\Com...\Interfaces\CcIResource suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
261
        foreach ($deps as $idref) {
262
            if (null === $idref || '' === $idref) {
263
                continue;
264
            }
265
            $depEl = $doc->createElementNS($imscc, 'dependency');
266
            $depEl->setAttribute('identifierref', (string) $idref);
267
            $resourceEl->appendChild($depEl);
268
        }
269
270
        // Append to container.
271
        $xmlnode->appendChild($resourceEl);
272
273
        // Track in helpers if available and reflect identifier back into resource.
274
        if (property_exists($this, 'resources')) {
275
            $this->resources[$identifier] = $res;
276
        }
277
        $res->identifier = $identifier;
278
279
        return $resourceEl;
280
    }
281
282
    /**
283
     * === Metadata methods with signatures compatible to the base ===.
284
     *
285
     * @param mixed      $met
286
     * @param null|mixed $xmlnode
287
     */
288
289
    /**
290
     * Manifest-level metadata node.
291
     */
292
    public function createMetadataNode(&$met, DOMDocument &$doc, $xmlnode = null)
293
    {
294
        $imscc = $this->ccnamespaces['imscc'];
295
        $metaEl = $doc->createElementNS($imscc, 'metadata');
296
297
        if ($xmlnode instanceof DOMNode) {
298
            $xmlnode->appendChild($metaEl);
299
        }
300
301
        // If metadata object knows how to render itself, delegate.
302
        if (\is_object($met) && method_exists($met, 'generate')) {
303
            $met->generate($doc, $metaEl, $imscc);
304
        }
305
306
        return $metaEl;
307
    }
308
309
    /**
310
     * Resource-level metadata node.
311
     */
312
    public function createMetadataResourceNode(&$met, DOMDocument &$doc, $xmlnode = null)
313
    {
314
        $imscc = $this->ccnamespaces['imscc'];
315
316
        if ($xmlnode instanceof DOMNode) {
317
            if (\is_object($met) && method_exists($met, 'generate')) {
318
                $met->generate($doc, $xmlnode, $imscc);
319
            } else {
320
                $xmlnode->appendChild($doc->createElementNS($imscc, 'lom'));
321
            }
322
        }
323
324
        return $xmlnode;
325
    }
326
327
    /**
328
     * File-level metadata node.
329
     */
330
    public function createMetadataFileNode(&$met, DOMDocument &$doc, $xmlnode = null)
331
    {
332
        $imscc = $this->ccnamespaces['imscc'];
333
334
        if ($xmlnode instanceof DOMNode && \is_object($met) && method_exists($met, 'generate')) {
335
            $met->generate($doc, $xmlnode, $imscc);
336
        }
337
338
        return $xmlnode;
339
    }
340
341
    /**
342
     * Educational metadata (role etc.). Keep signature identical to parent (no strict types).
343
     */
344
    public function createMetadataEducational($met, DOMDocument &$doc, $xmlnode)
345
    {
346
        $metadata = $doc->createElementNS($this->ccnamespaces['imscc'], 'metadata');
347
        if ($xmlnode instanceof DOMNode) {
348
            $xmlnode->insertBefore($metadata, $xmlnode->firstChild);
349
        }
350
        $lom = $doc->createElementNS($this->ccnamespaces['lom'], 'lom');
351
        $metadata->appendChild($lom);
352
        $educational = $doc->createElementNS($this->ccnamespaces['lom'], 'educational');
353
        $lom->appendChild($educational);
354
355
        $values = isset($met->arrayeducational) ? $met->arrayeducational : [];
356
        foreach ($values as $value) {
357
            $arr = \is_array($value) ? $value : [$value];
358
            foreach ($arr as $v) {
359
                $userrole = $doc->createElementNS($this->ccnamespaces['lom'], 'intendedEndUserRole');
360
                $educational->appendChild($userrole);
361
                $nd4 = $doc->createElementNS($this->ccnamespaces['lom'], 'source', 'IMSGLC_CC_Rolesv1p2');
362
                $nd5 = $doc->createElementNS($this->ccnamespaces['lom'], 'value', (string) $v);
363
                $userrole->appendChild($nd4);
364
                $userrole->appendChild($nd5);
365
            }
366
        }
367
368
        return $metadata;
369
    }
370
371
    /**
372
     * Render <item> nodes under an organization (recursive).
373
     * Signature matches parent (types kept loose).
374
     *
375
     * @param mixed $items
376
     */
377
    protected function updateItems($items, DOMDocument &$doc, DOMElement &$xmlnode): void
378
    {
379
        foreach ($items as $key => $item) {
380
            $itemnode = $doc->createElementNS($this->ccnamespaces['imscc'], 'item');
381
382
            if (method_exists($this, 'updateAttribute')) {
383
                $this->updateAttribute($doc, 'identifier', (string) $key, $itemnode);
384
                $this->updateAttribute(
385
                    $doc,
386
                    'identifierref',
387
                    isset($item->identifierref) ? (string) $item->identifierref : null,
388
                    $itemnode
389
                );
390
            } else {
391
                $itemnode->setAttribute('identifier', (string) $key);
392
                if (!empty($item->identifierref)) {
393
                    $itemnode->setAttribute('identifierref', (string) $item->identifierref);
394
                }
395
            }
396
397
            if (isset($item->title) && null !== $item->title) {
398
                $titlenode = $doc->createElementNS($this->ccnamespaces['imscc'], 'title');
399
                $titlenode->appendChild(new DOMText((string) $item->title));
400
                $itemnode->appendChild($titlenode);
401
            }
402
403
            if (method_exists($item, 'hasChildItems') && $item->hasChildItems() && isset($item->childitems)) {
404
                $this->updateItems((array) $item->childitems, $doc, $itemnode);
405
            }
406
407
            $xmlnode->appendChild($itemnode);
408
        }
409
    }
410
}
411