ILIAS  trunk Revision v11.0_alpha-1811-gd2d5443e411
All Data Structures Namespaces Files Functions Variables Enumerations Enumerator Modules Pages
ilSCORM13Package.php
Go to the documentation of this file.
1 <?php
2 
19 declare(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 }
$res
Definition: ltiservices.php:66
bool DOMDocument $manifest
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="")
__construct(?int $packageId=null)
$ilErr
Definition: raiseError.php:33
$path
Definition: ltiservices.php:29
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
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.
global $DIC
Definition: shib_login.php:22
static _refreshStatus(int $a_obj_id, ?array $a_users=null)
static array $elements
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 _removeTrackingDataForUser(int $user_id)
$r