ILIAS  release_8 Revision v8.24
ilSCORM13Package.php
Go to the documentation of this file.
1<?php
2
3declare(strict_types=1);
4
27{
28 public const DB_ENCODE_XSL = './Modules/Scorm2004/templates/xsl/op/op-scorm13.xsl';
29 public const CONVERT_XSL = './Modules/Scorm2004/templates/xsl/op/scorm12To2004.xsl';
30 public const DB_DECODE_XSL = './Modules/Scorm2004/templates/xsl/op/op-scorm13-revert.xsl';
31 public const VALIDATE_XSD = './libs/ilias/Scorm2004/xsd/op/op-scorm13.xsd';
32
33 public const WRAPPER_HTML = './Modules/Scorm2004/scripts/converter/GenericRunTimeWrapper1.0_aadlc/GenericRunTimeWrapper.htm';
34 public const WRAPPER_JS = './Modules/Scorm2004/scripts/converter/GenericRunTimeWrapper1.0_aadlc/SCOPlayerWrapper.js';
35
36
37 // private $packageFile;
38 private string $packageFolder;
39 private string $packagesFolder;
40 private array $packageData = [];
41 // private $slm;
42 // private $slm_tree;
43
44 public \DOMDocument $imsmanifest;
48 public $manifest;
49 public array $diagnostic;
50 // public $status;
51 public int $packageId;
52 public string $packageName = "";
53 public string $packageHash = "";
54 public int $userId;
55
56 // private $idmap = array();
57 private float $progress = 0.0;
58
62 private static array $elements = array(
63 'cp' => array(
64 'manifest',
65 'organization',
66 'item',
67 'hideLMSUI',
68 'resource',
69 'file',
70 'dependency',
71 'sequencing',
72 'rule',
73 'auxilaryResource',
74 'condition',
75 'mapinfo',
76 'objective',
77 ),
78 'cmi' => array(
79 'comment',
80 'correct_response',
81 'interaction',
82 'node',
83 'objective',
84 ),
85 );
86
87 public function __construct(?int $packageId = null)
88 {
89 $this->packagesFolder = ''; // #25372
90 if ($packageId != null) {
91 $this->load($packageId);
92 }
93 }
94
95 public function load(int $packageId): void
96 {
97 global $DIC;
98 $ilDB = $DIC->database();
99
100 $lm_set = $ilDB->queryF('SELECT * FROM sahs_lm WHERE id = %s', array('integer'), array($packageId));
101 $lm_data = $ilDB->fetchAssoc($lm_set);
102 $pg_set = $ilDB->queryF('SELECT * FROM cp_package WHERE obj_id = %s', array('integer'), array($packageId));
103 $pg_data = $ilDB->fetchAssoc($lm_set);
104
105 $this->packageData = array_merge($lm_data, $pg_data);
106 $this->packageId = $packageId;
107 $this->packageFolder = $this->packagesFolder . '/' . $packageId;
108 $this->packageFile = $this->packageFolder . '.zip';
109 $this->imsmanifestFile = $this->packageFolder . '/' . 'imsmanifest.xml';
110 }
111
117 public function il_import(string $packageFolder, int $packageId, bool $reimport = false)
118 {
119 global $DIC;
120 $ilDB = $DIC->database();
121 $ilLog = ilLoggerFactory::getLogger('sc13');
122 $ilErr = $DIC['ilErr'];
123
124 $title = "";
125
126 if ($reimport === true) {
127 $this->packageId = $packageId;
128 $this->dbRemoveAll();
129 }
130
131 $this->packageData['persistprevattempts'] = 0;
132 $this->packageData['default_lesson_mode'] = 'normal';
133 $this->packageData['credit'] = 'credit';
134 $this->packageData['auto_review'] = 'n';
135
136 $this->packageFolder = $packageFolder;
137 $this->packageId = $packageId;
138 $this->imsmanifestFile = $this->packageFolder . '/' . 'imsmanifest.xml';
139 //step 1 - parse Manifest-File and validate
140 $this->imsmanifest = new DOMDocument();
141 $this->imsmanifest->async = false;
142 if (!@$this->imsmanifest->load($this->imsmanifestFile)) {
143 $this->diagnostic[] = 'XML not wellformed';
144 return false;
145 }
146
147 //step 2 tranform
148 $this->manifest = $this->transform($this->imsmanifest, self::DB_ENCODE_XSL);
149
150 if (!$this->manifest) {
151 $this->diagnostic[] = 'Cannot transform into normalized manifest';
152 return false;
153 }
154 //setp 2.5 if only a single item, make sure the scormType of it's linked resource is SCO
155 $path = new DOMXpath($this->manifest);
156 $path->registerNamespace("scorm", "http://www.openpalms.net/scorm/scorm13");
157 $items = $path->query("//scorm:item");
158 if ($items->length == 1) {
159 $n = $items->item(0);
160 $resource = $path->query("//scorm:resource");//[&id='"+$n->getAttribute("resourceId")+"']");
161 foreach ($resource as $res) {
162 if ($n !== null && $res->getAttribute('id') == $n->getAttribute("resourceId")) {
163 $res->setAttribute('scormType', 'sco');
164 }
165 }
166 }
167 $this->dbImport($this->manifest);
168
169 if (file_exists($this->packageFolder . '/' . 'index.xml')) {
170 $doc = simplexml_load_file($this->packageFolder . '/' . 'index.xml');//PHP8Review: This may cause no trouble here but i still worth a look: https://bugs.php.net/bug.php?id=62577
171 $l = $doc->xpath("/ContentObject/MetaData");
172 if ($l[0]) {
173 $mdxml = new ilMDXMLCopier($l[0]->asXML(), $packageId, $packageId, ilObject::_lookupType($packageId));
174 $mdxml->startParsing();
175 $mdo = $mdxml->getMDObject();
176 if ($mdo) {
177 $mdo->update();
178 }
179 }
180 } else {
181 $importer = new ilSCORM13MDImporter($this->imsmanifest, $packageId);
182 $importer->import();
183 $title = $importer->getTitle();
184 $description = $importer->getDescription();
185 if ($description != "") {
187 }
188 }
189
190 //step 5
191 $x = simplexml_load_string($this->manifest->saveXML());
192 $x['persistPreviousAttempts'] = $this->packageData['persistprevattempts'];
193 // $x['online'] = !$this->getOfflineStatus();//$this->packageData['c_online'];
194
195 $x['defaultLessonMode'] = $this->packageData['default_lesson_mode'];
196 $x['credit'] = $this->packageData['credit'];
197 $x['autoReview'] = $this->packageData['auto_review'];
198 $j = array();
199 // first read resources into flat array to resolve item/identifierref later
200 $r = array();
201 foreach ($x->resource as $xe) {
202 $r[strval($xe['id'])] = $xe;
203 unset($xe);
204 }
205 // iterate through items and set href and scoType as activity attributes
206 foreach ($x->xpath('//*[local-name()="item"]') as $xe) {
207 // get reference to resource and set href accordingly
208 if ($b = ($r[strval($xe['resourceId'])] ?? false)) {
209 $xe['href'] = strval($b['base']) . strval($b['href']);
210 unset($xe['resourceId']);
211 if (strval($b['scormType']) === 'sco') {
212 $xe['sco'] = true;
213 }
214 }
215 }
216 // iterate recursivly through activities and build up simple php object
217 // with items and associated sequencings
218 // top node is the default organization which is handled as an item
219 $this->jsonNode($x->organization, $j['item']);
220 foreach ($x->sequencing as $s) {
221 $this->jsonNode($s, $j['sequencing'][]);
222 }
223 // combined manifest+resources xml:base is set as organization base
224 $j['item']['base'] = strval($x['base']);
225 // package folder is base to whole playing process
226 $j['base'] = $packageFolder . '/';
227 $j['foreignId'] = floatval($x['foreignId']); // manifest cp_node_id for associating global (package wide) objectives
228 $j['id'] = strval($x['id']); // manifest id for associating global (package wide) objectives
229 $j['item']['title'] = ilUtil::stripSlashes($j['item']['title']);
230 for($i = 0; $i < count($j['item']['item']); $i++) {
231 $j['item']['item'][$i]['title'] = ilUtil::stripSlashes($j['item']['item'][$i]['title']);
232 }
233
234 //last step - build ADL Activity tree
235 $act = new SeqTreeBuilder();
236 $adl_tree = $act->buildNodeSeqTree($this->imsmanifestFile);
237 $ilDB->update(
238 'cp_package',
239 array(
240 'xmldata' => array('clob', $x->asXML()),
241 'jsdata' => array('clob', json_encode($j)),
242 'activitytree' => array('clob', json_encode($adl_tree['tree'])),
243 'global_to_system' => array('integer', (int) $adl_tree['global']),
244 'shared_data_global_to_system' => array('integer', (int) $adl_tree['dataglobal'])
245 ),
246 array(
247 'obj_id' => array('integer', (int) $this->packageId)
248 )
249 );
250
251 // title retrieved by importer
252 if ($title != "") {
253 return $title;
254 }
255
256 return $j['item']['title'];
257 }
258
259
267 public function jsonNode(object $node, ?array &$sink): void
268 {
269 foreach ($node->attributes() as $k => $v) {
270 // cast to boolean and number if possible
271 $v = strval($v);
272 if ($v === "true") {
273 $v = true;
274 } elseif ($v === "false") {
275 $v = false;
276 } elseif (is_numeric($v)) {
277 $v = (float) $v;
278 }
279 $sink[$k] = $v;
280 }
281 foreach ($node->children() as $name => $child) {
282 self::jsonNode($child, $sink[$name][]); // RECURSION
283 }
284 }
285
286 public function dbImport(object $node, ?int &$lft = 1, ?int $depth = 1, ?int $parent = 0): void
287 {
288 global $DIC;
289 $ilDB = $DIC->database();
290
291 switch ($node->nodeType) {
292 case XML_DOCUMENT_NODE:
293
294 // insert into cp_package
295
296 $res = $ilDB->queryF(
297 'SELECT * FROM cp_package WHERE obj_id = %s AND c_identifier = %s',
298 array('integer', 'text'),
299 array($this->packageId, $this->packageName)
300 );
301 if ($num_rows = $ilDB->numRows($res)) {
302 $query = 'UPDATE cp_package '
303 . 'SET persistprevattempts = %s, c_settings = %s '
304 . 'WHERE obj_id = %s AND c_identifier= %s';
305 $ilDB->manipulateF(
306 $query,
307 array('integer', 'text', 'integer', 'text'),
308 array(0, null, $this->packageId, $this->packageName)
309 );
310 } else {
311 $query = 'INSERT INTO cp_package (obj_id, c_identifier, persistprevattempts, c_settings) '
312 . 'VALUES (%s, %s, %s, %s)';
313 $ilDB->manipulateF(
314 $query,
315 array('integer','text','integer', 'text'),
316 array($this->packageId, $this->packageName, 0, null)
317 );
318 }
319
320 // run sub nodes
321 $this->dbImport($node->documentElement); // RECURSION
322 break;
323
324 case XML_ELEMENT_NODE:
325 if ($node->nodeName === 'manifest') {
326 if ($node->getAttribute('uri') == "") {
327 // default URI is md5 hash of zip file, i.e. packageHash
328 $node->setAttribute('uri', 'md5:' . $this->packageHash);
329 }
330 }
331
332 $cp_node_id = $ilDB->nextId('cp_node');
333
334 $query = 'INSERT INTO cp_node (cp_node_id, slm_id, nodename) '
335 . 'VALUES (%s, %s, %s)';
336 $ilDB->manipulateF(
337 $query,
338 array('integer', 'integer', 'text'),
339 array($cp_node_id, $this->packageId, $node->nodeName)
340 );
341
342 $query = 'INSERT INTO cp_tree (child, depth, lft, obj_id, parent, rgt) '
343 . 'VALUES (%s, %s, %s, %s, %s, %s)';
344 $ilDB->manipulateF(
345 $query,
346 array('integer', 'integer', 'integer', 'integer', 'integer', 'integer'),
347 array($cp_node_id, $depth, $lft++, $this->packageId, $parent, 0)
348 );
349
350 // insert into cp_*
351 //$a = array('cp_node_id' => $cp_node_id);
352 $names = array('cp_node_id');
353 $values = array($cp_node_id);
354 $types = array('integer');
355
356 foreach ($node->attributes as $attr) {
357 switch (strtolower($attr->name)) {
358 case 'completionsetbycontent': $names[] = 'completionbycontent';
359 break;
360 case 'objectivesetbycontent': $names[] = 'objectivebycontent';
361 break;
362 case 'type': $names[] = 'c_type';
363 break;
364 case 'mode': $names[] = 'c_mode';
365 break;
366 case 'language': $names[] = 'c_language';
367 break;
368 case 'condition': $names[] = 'c_condition';
369 break;
370 case 'operator': $names[] = 'c_operator';
371 break;
372 // case 'condition': $names[] = 'c_condition';break;
373 case 'readnormalizedmeasure': $names[] = 'readnormalmeasure';
374 break;
375 case 'writenormalizedmeasure': $names[] = 'writenormalmeasure';
376 break;
377 case 'minnormalizedmeasure': $names[] = 'minnormalmeasure';
378 break;
379 case 'primary': $names[] = 'c_primary';
380 break;
381 // case 'minnormalizedmeasure': $names[] = 'minnormalmeasure';break;
382 case 'persistpreviousattempts': $names[] = 'persistprevattempts';
383 break;
384 case 'identifier': $names[] = 'c_identifier';
385 break;
386 case 'settings': $names[] = 'c_settings';
387 break;
388 case 'activityabsolutedurationlimit': $names[] = 'activityabsdurlimit';
389 break;
390 case 'activityexperienceddurationlimit': $names[] = 'activityexpdurlimit';
391 break;
392 case 'attemptabsolutedurationlimit': $names[] = 'attemptabsdurlimit';
393 break;
394 case 'measuresatisfactionifactive': $names[] = 'measuresatisfactive';
395 break;
396 case 'objectivemeasureweight': $names[] = 'objectivemeasweight';
397 break;
398 case 'requiredforcompleted': $names[] = 'requiredcompleted';
399 break;
400 case 'requiredforincomplete': $names[] = 'requiredincomplete';
401 break;
402 case 'requiredfornotsatisfied': $names[] = 'requirednotsatisfied';
403 break;
404 case 'rollupobjectivesatisfied': $names[] = 'rollupobjectivesatis';
405 break;
406 case 'rollupprogresscompletion': $names[] = 'rollupprogcompletion';
407 break;
408 case 'usecurrentattemptobjectiveinfo': $names[] = 'usecurattemptobjinfo';
409 break;
410 case 'usecurrentattemptprogressinfo': $names[] = 'usecurattemptproginfo';
411 break;
412 default: $names[] = strtolower($attr->name);
413 break;
414 }
415
416 if (in_array(
417 $names[count($names) - 1],
418 array('flow', 'completionbycontent',
419 'objectivebycontent', 'rollupobjectivesatis',
420 'tracked', 'choice',
421 'choiceexit', 'satisfiedbymeasure',
422 'c_primary', 'constrainchoice',
423 'forwardonly', 'global_to_system',
424 'writenormalmeasure', 'writesatisfiedstatus',
425 'readnormalmeasure', 'readsatisfiedstatus',
426 'preventactivation', 'measuresatisfactive',
427 'reorderchildren', 'usecurattemptproginfo',
428 'usecurattemptobjinfo', 'rollupprogcompletion',
429 'read_shared_data', 'write_shared_data',
430 'shared_data_global_to_system', 'completedbymeasure')
431 )) {
432 if ($attr->value === 'true') {
433 $values[] = 1;
434 } elseif ($attr->value === 'false') {
435 $values[] = 0;
436 } else {
437 $values[] = (int) $attr->value;
438 }
439 } else {
440 $values[] = $attr->value;
441 }
442
443 if (in_array(
444 $names[count($names) - 1],
445 array('objectivesglobtosys', 'attemptlimit',
446 'flow', 'completionbycontent',
447 'objectivebycontent', 'rollupobjectivesatis',
448 'tracked', 'choice',
449 'choiceexit', 'satisfiedbymeasure',
450 'c_primary', 'constrainchoice',
451 'forwardonly', 'global_to_system',
452 'writenormalmeasure', 'writesatisfiedstatus',
453 'readnormalmeasure', 'readsatisfiedstatus',
454 'preventactivation', 'measuresatisfactive',
455 'reorderchildren', 'usecurattemptproginfo',
456 'usecurattemptobjinfo', 'rollupprogcompletion',
457 'read_shared_data', 'write_shared_data',
458 'shared_data_global_to_system')
459 )) {
460 $types[] = 'integer';
461 } elseif (in_array(
462 $names[count($names) - 1],
463 array('jsdata', 'xmldata', 'activitytree', 'data')
464 )) {
465 $types[] = 'clob';
466 } elseif ($names[count($names) - 1] === 'objectivemeasweight') {
467 $types[] = 'float';
468 } else {
469 $types[] = 'text';
470 }
471 }
472
473 if ($node->nodeName === 'datamap') {
474 $names[] = 'slm_id';
475 $values[] = $this->packageId;
476 $types[] = 'integer';
477
478 $names[] = 'sco_node_id';
479 $values[] = $parent;
480 $types[] = 'integer';
481 }
482
483 // we have to change the insert method because of clob fields ($ilDB->manipulate does not work here)
484 $insert_data = array();
485 foreach ($names as $key => $db_field) {
486 $insert_data[$db_field] = array($types[$key], trim((string) $values[$key]));
487 }
488 $ilDB->insert('cp_' . strtolower($node->nodeName), $insert_data);
489
490 $node->setAttribute('foreignId', (string) $cp_node_id);
491 $this->idmap[$node->getAttribute('id')] = $cp_node_id;
492
493 // run sub nodes
494 foreach ($node->childNodes as $child) {
495 $this->dbImport($child, $lft, $depth + 1, $cp_node_id); // RECURSION
496 }
497
498 // update cp_tree (rgt value for pre order walk in sql tree)
499 $query = 'UPDATE cp_tree SET rgt = %s WHERE child = %s';
500 $ilDB->manipulateF(
501 $query,
502 array('integer', 'integer'),
503 array($lft++, $cp_node_id)
504 );
505
506 break;
507 }
508 }
509
510
511 public function removeCMIData(): void
512 {
514 ilLPStatusWrapper::_refreshStatus($this->packageId);
515 }
516
517 public function removeCPData(): void
518 {
519 global $DIC;
520 $ilDB = $DIC->database();
521 $ilLog = ilLoggerFactory::getLogger('sc13');
522
523 //get relevant nodes
524 $cp_nodes = array();
525
526 $res = $ilDB->queryF(
527 'SELECT cp_node.cp_node_id FROM cp_node WHERE cp_node.slm_id = %s',
528 array('integer'),
529 array($this->packageId)
530 );
531 while ($data = $ilDB->fetchAssoc($res)) {
532 $cp_nodes[] = $data['cp_node_id'];
533 }
534
535 //remove package data
536 foreach (self::$elements['cp'] as $t) {
537 $t = 'cp_' . $t;
538
539 $in = $ilDB->in(strtolower($t) . '.cp_node_id', $cp_nodes, false, 'integer');
540 $ilDB->manipulate('DELETE FROM ' . strtolower($t) . ' WHERE ' . $in);
541 }
542
543 // remove CP structure entries in tree and node
544 $ilDB->manipulateF(
545 'DELETE FROM cp_tree WHERE cp_tree.obj_id = %s',
546 array('integer'),
547 array($this->packageId)
548 );
549
550 $ilDB->manipulateF(
551 'DELETE FROM cp_node WHERE cp_node.slm_id = %s',
552 array('integer'),
553 array($this->packageId)
554 );
555
556 // remove general package entry
557 $ilDB->manipulateF(
558 'DELETE FROM cp_package WHERE cp_package.obj_id = %s',
559 array('integer'),
560 array($this->packageId)
561 );
562 }
563
564 public function dbRemoveAll(): void
565 {
566 //dont change order of calls
567 $this->removeCMIData();
568 $this->removeCPData();
569 }
570
574 public function transform(\DOMDocument $inputdoc, string $xslfile, ?string $outputpath = null)
575 {
576 $xsl = new DOMDocument();
577 $xsl->async = false;
578 if (!@$xsl->load($xslfile)) {
579 die('ERROR: load StyleSheet ' . $xslfile);
580 }
581 $prc = new XSLTProcessor();
582 $prc->registerPHPFunctions();
583 $r = @$prc->importStyleSheet($xsl);
584 if (false === @$prc->importStyleSheet($xsl)) {
585 die('ERROR: importStyleSheet ' . $xslfile);
586 }
587 if ($outputpath) {
588 file_put_contents($outputpath, $prc->transformToXML($inputdoc));
589 } else {
590 return $prc->transformToDoc($inputdoc);
591 }
592 }
593
594 //to be called from IlObjUser
595 public static function _removeTrackingDataForUser(int $user_id): void
596 {
598 }
599}
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static _refreshStatus(int $a_obj_id, ?array $a_users=null)
static getLogger(string $a_component_id)
Get component logger.
static _lookupType(int $id, bool $reference=false)
static _writeDescription(int $obj_id, string $desc)
write description to db (static)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
dbImport(object $node, ?int &$lft=1, ?int $depth=1, ?int $parent=0)
DOMDocument $imsmanifest
jsonNode(object $node, ?array &$sink)
Helper for UploadAndImport Recursively copies values from XML into PHP array for export as json Eleme...
load(int $packageId)
transform(\DOMDocument $inputdoc, string $xslfile, ?string $outputpath=null)
static array $elements
static _removeTrackingDataForUser(int $user_id)
__construct(?int $packageId=null)
il_import(string $packageFolder, int $packageId, bool $reimport=false)
Imports an extracted SCORM 2004 module from ilias-data dir into database.
static removeCMIDataForPackage(int $packageId)
static removeCMIDataForUser(int $user_id)
static stripSlashes(string $a_str, bool $a_strip_html=true, string $a_allow="")
global $DIC
Definition: feed.php:28
$path
Definition: ltiservices.php:32
$res
Definition: ltiservices.php:69
if($format !==null) $name
Definition: metadata.php:247
$i
Definition: metadata.php:41
string $key
Consumer key/client ID value.
Definition: System.php:193
$query
$ilErr
Definition: raiseError.php:17
$lm_set