ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
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 'condition': $names[] = 'c_condition';break;
349 case 'readnormalizedmeasure': $names[] = 'readnormalmeasure';
350 break;
351 case 'writenormalizedmeasure': $names[] = 'writenormalmeasure';
352 break;
353 case 'minnormalizedmeasure': $names[] = 'minnormalmeasure';
354 break;
355 case 'primary': $names[] = 'c_primary';
356 break;
357 // case 'minnormalizedmeasure': $names[] = 'minnormalmeasure';break;
358 case 'persistpreviousattempts': $names[] = 'persistprevattempts';
359 break;
360 case 'identifier': $names[] = 'c_identifier';
361 break;
362 case 'settings': $names[] = 'c_settings';
363 break;
364 case 'activityabsolutedurationlimit': $names[] = 'activityabsdurlimit';
365 break;
366 case 'activityexperienceddurationlimit': $names[] = 'activityexpdurlimit';
367 break;
368 case 'attemptabsolutedurationlimit': $names[] = 'attemptabsdurlimit';
369 break;
370 case 'measuresatisfactionifactive': $names[] = 'measuresatisfactive';
371 break;
372 case 'objectivemeasureweight': $names[] = 'objectivemeasweight';
373 break;
374 case 'requiredforcompleted': $names[] = 'requiredcompleted';
375 break;
376 case 'requiredforincomplete': $names[] = 'requiredincomplete';
377 break;
378 case 'requiredfornotsatisfied': $names[] = 'requirednotsatisfied';
379 break;
380 case 'rollupobjectivesatisfied': $names[] = 'rollupobjectivesatis';
381 break;
382 case 'rollupprogresscompletion': $names[] = 'rollupprogcompletion';
383 break;
384 case 'usecurrentattemptobjectiveinfo': $names[] = 'usecurattemptobjinfo';
385 break;
386 case 'usecurrentattemptprogressinfo': $names[] = 'usecurattemptproginfo';
387 break;
388 default: $names[] = strtolower($attr->name);
389 break;
390 }
391
392 if (in_array(
393 $names[count($names) - 1],
394 array('flow', 'completionbycontent',
395 'objectivebycontent', 'rollupobjectivesatis',
396 'tracked', 'choice',
397 'choiceexit', 'satisfiedbymeasure',
398 'c_primary', 'constrainchoice',
399 'forwardonly', 'global_to_system',
400 'writenormalmeasure', 'writesatisfiedstatus',
401 'readnormalmeasure', 'readsatisfiedstatus',
402 'preventactivation', 'measuresatisfactive',
403 'reorderchildren', 'usecurattemptproginfo',
404 'usecurattemptobjinfo', 'rollupprogcompletion',
405 'read_shared_data', 'write_shared_data',
406 'shared_data_global_to_system', 'completedbymeasure')
407 )) {
408 if ($attr->value === 'true') {
409 $values[] = 1;
410 } elseif ($attr->value === 'false') {
411 $values[] = 0;
412 } else {
413 $values[] = (int) $attr->value;
414 }
415 } else {
416 $values[] = $attr->value;
417 }
418
419 if (in_array(
420 $names[count($names) - 1],
421 array('objectivesglobtosys', 'attemptlimit',
422 'flow', 'completionbycontent',
423 'objectivebycontent', 'rollupobjectivesatis',
424 'tracked', 'choice',
425 'choiceexit', 'satisfiedbymeasure',
426 'c_primary', 'constrainchoice',
427 'forwardonly', 'global_to_system',
428 'writenormalmeasure', 'writesatisfiedstatus',
429 'readnormalmeasure', 'readsatisfiedstatus',
430 'preventactivation', 'measuresatisfactive',
431 'reorderchildren', 'usecurattemptproginfo',
432 'usecurattemptobjinfo', 'rollupprogcompletion',
433 'read_shared_data', 'write_shared_data',
434 'shared_data_global_to_system')
435 )) {
436 $types[] = 'integer';
437 } elseif (in_array(
438 $names[count($names) - 1],
439 array('jsdata', 'xmldata', 'activitytree', 'data')
440 )) {
441 $types[] = 'clob';
442 } elseif ($names[count($names) - 1] === 'objectivemeasweight') {
443 $types[] = 'float';
444 } else {
445 $types[] = 'text';
446 }
447 }
448
449 if ($node->nodeName === 'datamap') {
450 $names[] = 'slm_id';
451 $values[] = $this->packageId;
452 $types[] = 'integer';
453
454 $names[] = 'sco_node_id';
455 $values[] = $parent;
456 $types[] = 'integer';
457 }
458
459 // we have to change the insert method because of clob fields ($ilDB->manipulate does not work here)
460 $insert_data = array();
461 foreach ($names as $key => $db_field) {
462 $insert_data[$db_field] = array($types[$key], trim((string) $values[$key]));
463 }
464 $ilDB->insert('cp_' . strtolower($node->nodeName), $insert_data);
465
466 $node->setAttribute('foreignId', (string) $cp_node_id);
467 $this->idmap[$node->getAttribute('id')] = $cp_node_id;
468
469 // run sub nodes
470 foreach ($node->childNodes as $child) {
471 $this->dbImport($child, $lft, $depth + 1, $cp_node_id); // RECURSION
472 }
473
474 // update cp_tree (rgt value for pre order walk in sql tree)
475 $query = 'UPDATE cp_tree SET rgt = %s WHERE child = %s';
476 $ilDB->manipulateF(
477 $query,
478 array('integer', 'integer'),
479 array($lft++, $cp_node_id)
480 );
481
482 break;
483 }
484 }
485
486
487 public function removeCMIData(): void
488 {
490 ilLPStatusWrapper::_refreshStatus($this->packageId);
491 }
492
493 public function removeCPData(): void
494 {
495 global $DIC;
496 $ilDB = $DIC->database();
497 $ilLog = ilLoggerFactory::getLogger('sc13');
498
499 //get relevant nodes
500 $cp_nodes = array();
501
502 $res = $ilDB->queryF(
503 'SELECT cp_node.cp_node_id FROM cp_node WHERE cp_node.slm_id = %s',
504 array('integer'),
505 array($this->packageId)
506 );
507 while ($data = $ilDB->fetchAssoc($res)) {
508 $cp_nodes[] = $data['cp_node_id'];
509 }
510
511 //remove package data
512 foreach (self::$elements['cp'] as $t) {
513 $t = 'cp_' . $t;
514
515 $in = $ilDB->in(strtolower($t) . '.cp_node_id', $cp_nodes, false, 'integer');
516 $ilDB->manipulate('DELETE FROM ' . strtolower($t) . ' WHERE ' . $in);
517 }
518
519 // remove CP structure entries in tree and node
520 $ilDB->manipulateF(
521 'DELETE FROM cp_tree WHERE cp_tree.obj_id = %s',
522 array('integer'),
523 array($this->packageId)
524 );
525
526 $ilDB->manipulateF(
527 'DELETE FROM cp_node WHERE cp_node.slm_id = %s',
528 array('integer'),
529 array($this->packageId)
530 );
531
532 // remove general package entry
533 $ilDB->manipulateF(
534 'DELETE FROM cp_package WHERE cp_package.obj_id = %s',
535 array('integer'),
536 array($this->packageId)
537 );
538 }
539
540 public function dbRemoveAll(): void
541 {
542 //dont change order of calls
543 $this->removeCMIData();
544 $this->removeCPData();
545 }
546
550 public function transform(\DOMDocument $inputdoc, string $xslfile, ?string $outputpath = null)
551 {
552 $xsl = new DOMDocument();
553 // $xsl->async = false;
554 if (!@$xsl->load($xslfile)) {
555 die('ERROR: load StyleSheet ' . $xslfile);
556 }
557 $prc = new XSLTProcessor();
558 $prc->registerPHPFunctions();
559 $r = @$prc->importStyleSheet($xsl);
560 if (false === @$prc->importStyleSheet($xsl)) {
561 die('ERROR: importStyleSheet ' . $xslfile);
562 }
563 if ($outputpath) {
564 file_put_contents($outputpath, $prc->transformToXML($inputdoc));
565 } else {
566 return $prc->transformToDoc($inputdoc);
567 }
568 }
569
570 //to be called from IlObjUser
571 public static function _removeTrackingDataForUser(int $user_id): void
572 {
574 }
575}
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