ILIAS  release_8 Revision v8.19
All Data Structures Namespaces Files Functions Variables Modules Pages
ilSCORM13Package.php
Go to the documentation of this file.
1 <?php
2 
3 declare(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 != "") {
186  ilObject::_writeDescription($packageId, $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 }
$res
Definition: ltiservices.php:69
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static getLogger(string $a_component_id)
Get component logger.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
load(int $packageId)
static stripSlashes(string $a_str, bool $a_strip_html=true, string $a_allow="")
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
__construct(?int $packageId=null)
$ilErr
Definition: raiseError.php:17
$path
Definition: ltiservices.php:32
global $DIC
Definition: feed.php:28
if($format !==null) $name
Definition: metadata.php:247
dbImport(object $node, ?int &$lft=1, ?int $depth=1, ?int $parent=0)
il_import(string $packageFolder, int $packageId, bool $reimport=false)
Imports an extracted SCORM 2004 module from ilias-data dir into database.
string $key
Consumer key/client ID value.
Definition: System.php:193
static _refreshStatus(int $a_obj_id, ?array $a_users=null)
static array $elements
$query
jsonNode(object $node, ?array &$sink)
Helper for UploadAndImport Recursively copies values from XML into PHP array for export as json Eleme...
transform(\DOMDocument $inputdoc, string $xslfile, ?string $outputpath=null)
DOMDocument $imsmanifest
$lm_set
static removeCMIDataForUser(int $user_id)
static removeCMIDataForPackage(int $packageId)
static _lookupType(int $id, bool $reference=false)
static _removeTrackingDataForUser(int $user_id)
static _writeDescription(int $obj_id, string $desc)
write description to db (static)
$i
Definition: metadata.php:41