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