ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
class.ilObjSCORMLearningModule.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
30 {
36  public function __construct(int $a_id = 0, bool $a_call_by_reference = true)
37  {
38  $this->type = "sahs";
39  parent::__construct($a_id, $a_call_by_reference);
40  }
41 
42  public function getTrackingItems(): array
43  {
45  }
46 
50  public static function _getTrackingItems(int $a_obj_id): array
51  {
52  $tree = new ilSCORMTree($a_obj_id);
53  $root_id = $tree->readRootId();
54 
55  $items = array();
56  $childs = $tree->getSubTree($tree->getNodeData($root_id));
57 
58  foreach ($childs as $child) {
59  if ($child["c_type"] === "sit") {
60  $sc_item = new ilSCORMItem((int) $child["obj_id"]);
61  if ($sc_item->getIdentifierRef() != "") {
62  $items[] = $sc_item;
63  }
64  }
65  }
66 
67  return $items;
68  }
69 
74  public function readObject(): string
75  {
76  global $DIC;
77  $ilErr = $DIC['ilErr'];
78 
79  $needs_convert = false;
80 
81  // convert imsmanifest.xml file in iso to utf8 if needed
82 
83  $manifest_file = $this->getDataDirectory() . "/imsmanifest.xml";
84 
85  // check if manifestfile exists and space left on device...
86  $check_for_manifest_file = is_file($manifest_file);
87 
88  // if no manifestfile
89  if (!$check_for_manifest_file) {
90  $ilErr->raiseError($this->lng->txt("Manifestfile $manifest_file not found!"), $ilErr->MESSAGE);
91  return "";
92  }
93 
94  if ($check_for_manifest_file) {
95  $manifest_file_array = file($manifest_file);
96  foreach ($manifest_file_array as $mfa) {
97  // if (seems_not_utf8($mfa))
98  if (@iconv('UTF-8', 'UTF-8', $mfa) != $mfa) {
99  $needs_convert = true;
100  break;
101  }
102  }
103 
104  // to copy the file we need some extraspace, counted in bytes *2 ... we need 2 copies....
105  $estimated_manifest_filesize = filesize($manifest_file) * 2;
106 
107  // i deactivated this, because it seems to fail on some windows systems (see bug #1795)
108  //$check_disc_free = disk_free_space($this->getDataDirectory()) - $estimated_manifest_filesize;
109  $check_disc_free = 2;
110  }
111 
112  // if $manifest_file needs to be converted to UTF8
113  if ($needs_convert) {
114  // if file exists and enough space left on device
115  if ($check_for_manifest_file && ($check_disc_free > 1)) {
116  // create backup from original
117  if (!copy($manifest_file, $manifest_file . ".old")) {
118  echo "Failed to copy $manifest_file...<br>\n";
119  }
120 
121  // read backupfile, convert each line to utf8, write line to new file
122  // php < 4.3 style
123  $f_write_handler = fopen($manifest_file . ".new", "w");
124  $f_read_handler = fopen($manifest_file . ".old", "r");
125  while (!feof($f_read_handler)) {
126  $zeile = fgets($f_read_handler);
127  //echo mb_detect_encoding($zeile);
128  fwrite($f_write_handler, utf8_encode($zeile));
129  }
130  fclose($f_read_handler);
131  fclose($f_write_handler);
132 
133  // copy new utf8-file to imsmanifest.xml
134  if (!copy($manifest_file . ".new", $manifest_file)) {
135  echo "Failed to copy $manifest_file...<br>\n";
136  }
137 
138  if (!@is_file($manifest_file)) {
139  $ilErr->raiseError($this->lng->txt("cont_no_manifest"), $ilErr->WARNING);
140  }
141  } else {
142  // gives out the specific error
143 
144  if (!($check_disc_free > 1)) {
145  $ilErr->raiseError($this->lng->txt("Not enough space left on device!"), $ilErr->MESSAGE);
146  }
147  return "";
148  }
149  } else {
150  // check whether file starts with BOM (that confuses some sax parsers, see bug #1795)
151  $hmani = fopen($manifest_file, "r");
152  $start = fread($hmani, 3);
153  if (strtolower(bin2hex($start)) === "efbbbf") {
154  $f_write_handler = fopen($manifest_file . ".new", "w");
155  while (!feof($hmani)) {
156  $n = fread($hmani, 900);
157  fwrite($f_write_handler, $n);
158  }
159  fclose($f_write_handler);
160  fclose($hmani);
161 
162  // copy new utf8-file to imsmanifest.xml
163  if (!copy($manifest_file . ".new", $manifest_file)) {
164  echo "Failed to copy $manifest_file...<br>\n";
165  }
166  } else {
167  fclose($hmani);
168  }
169  }
170 
171  // todo determine imsmanifest.xml path here...
172  $slmParser = new ilSCORMPackageParser($this, $manifest_file);
173  $slmParser->startParsing();
174  return (string) $slmParser->getPackageTitle();
175  }
176 
180  public function setLearningProgressSettingsAtUpload(): void
181  {
182  global $DIC;
183  $ilSetting = $DIC->settings();
184  //condition 1
185  $lm_set = new ilSetting("lm");
186  if ($lm_set->get('scorm_lp_auto_activate') != 1) {
187  return;
188  }
190  return;
191  }
192  $lm_set = new ilLPObjSettings($this->getId());
194  $lm_set->insert();
195  $collection = new ilLPCollectionOfSCOs($this->getId(), ilLPObjSettings::LP_MODE_SCORM);
196  $scos = array();
197  foreach ($collection->getPossibleItems() as $sco_id => $item) {
198  $scos[] = $sco_id;
199  }
200  $collection->activateEntries($scos);
201  }
202 
206  public function getTrackedItems(): array
207  {
208  global $DIC;
209  $ilDB = $DIC->database();
210  $ilUser = $DIC->user();
211 
212  $sco_set = $ilDB->queryF(
213  '
214  SELECT DISTINCT sco_id FROM scorm_tracking WHERE obj_id = %s',
215  array('integer'),
216  array($this->getId())
217  );
218 
219  $items = array();
220  while ($sco_rec = $ilDB->fetchAssoc($sco_set)) {
221  $sc_item = new ilSCORMItem((int) $sco_rec["sco_id"]);
222  if ($sc_item->getIdentifierRef() != "") {
223  $items[] = $sc_item;
224  }
225  }
226 
227  return $items;
228  }
229 
230  // /**
231  // * Return the last access timestamp for a given user
232  // *
233  // * @param int $a_obj_id object id
234  // * @param int $user_id user id
235  // * @return timestamp
236  // */
237  // public static function _lookupLastAccess(int $a_obj_id, $a_usr_id)
238  // {
239  // global $DIC;
240  // $ilDB = $DIC->database();
241  //
242  // $result = $ilDB->queryF(
243  // '
244  // SELECT last_access FROM sahs_user
245  // WHERE obj_id = %s
246  // AND user_id = %s',
247  // array('integer','integer'),
248  // array($a_obj_id,$a_usr_id)
249  // );
250  //
251  // if ($ilDB->numRows($result)) {
252  // $row = $ilDB->fetchAssoc($result);
253  // return $row["last_access"];
254  // }
255  // return "";
256  // }
257 
258  public function getTrackedUsers(string $a_search): array
259  {
260  global $DIC;
261  $ilDB = $DIC->database();
262  $ilUser = $DIC->user();
263  //TODO: UK last_access is not correct if no Commit or last_visited_sco
264  // $query = 'SELECT user_id,MAX(c_timestamp) last_access, lastname, firstname FROM scorm_tracking st ' .
265  $query = 'SELECT user_id, last_access, lastname, firstname FROM sahs_user st ' .
266  'JOIN usr_data ud ON st.user_id = ud.usr_id ' .
267  'WHERE obj_id = ' . $ilDB->quote($this->getId(), 'integer');
268  if ($a_search) {
269  // $query .= ' AND (' . $ilDB->like('lastname', 'text', '%' . $a_search . '%') . ' OR ' . $ilDB->like('firstname', 'text', '%' . $a_search . '%') .')';
270  $query .= ' AND ' . $ilDB->like('lastname', 'text', '%' . $a_search . '%');
271  }
272  $query .= ' GROUP BY user_id, lastname, firstname, last_access';
273  $sco_set = $ilDB->query($query);
274 
275  $items = array();
276  while ($sco_rec = $ilDB->fetchAssoc($sco_set)) {
277  $items[] = $sco_rec;
278  }
279  return $items;
280  }
281 
282  //toDo
288  public function getAttemptsForUsers(): array
289  {
290  global $DIC;
291  $ilDB = $DIC->database();
292  $query = 'SELECT user_id, package_attempts FROM sahs_user WHERE obj_id = ' . $ilDB->quote($this->getId(), 'integer') . ' ';
293  $res = $ilDB->query($query);
294 
295  $attempts = array();
296  while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) {
297  $attempts[$row['user_id']] = (int) $row['package_attempts'];
298  }
299  return $attempts;
300  }
301 
302  //todo
303 
307  public function getAttemptsForUser(int $a_user_id): int
308  {
309  global $DIC;
310  $ilDB = $DIC->database();
311  $val_set = $ilDB->queryF(
312  'SELECT package_attempts FROM sahs_user WHERE obj_id = %s AND user_id = %s',
313  array('integer','integer'),
314  array($this->getId(),$a_user_id)
315  );
316 
317  $val_rec = $ilDB->fetchAssoc($val_set);
318 
319  if ($val_rec["package_attempts"] == null) {
320  $val_rec["package_attempts"] = 0;
321  }
322  return (int) $val_rec["package_attempts"];
323  }
324 
328  public function getModuleVersionForUsers(): array
329  {
330  global $DIC;
331  $ilDB = $DIC->database();
332  $query = 'SELECT user_id, module_version FROM sahs_user WHERE obj_id = ' . $ilDB->quote($this->getId(), 'integer') . ' ';
333  $res = $ilDB->query($query);
334 
335  $versions = array();
336  while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) {
337  $versions[$row['user_id']] = (int) $row['module_version'];
338  }
339  return $versions;
340  }
341 
345  public function getModuleVersionForUser(int $a_user_id): string
346  {
347  global $DIC;
348  $ilDB = $DIC->database();
349  $val_set = $ilDB->queryF(
350  'SELECT module_version FROM sahs_user WHERE obj_id = %s AND user_id = %s',
351  array('integer','integer'),
352  array($this->getId(),$a_user_id,0)
353  );
354 
355  $val_rec = $ilDB->fetchAssoc($val_set);
356 
357  if ($val_rec["module_version"] == null) {
358  $val_rec["module_version"] = "";
359  }
360  return $val_rec["module_version"];
361  }
362 
366  public function getTrackingDataPerUser(int $a_sco_id, int $a_user_id): array
367  {
368  global $DIC;
369  $ilDB = $DIC->database();
370 
371  $data_set = $ilDB->queryF(
372  '
373  SELECT * FROM scorm_tracking
374  WHERE user_id = %s
375  AND sco_id = %s
376  AND obj_id = %s
377  ORDER BY lvalue',
378  array('integer','integer','integer'),
379  array($a_user_id,$a_sco_id,$this->getId())
380  );
381 
382  $data = array();
383  while ($data_rec = $ilDB->fetchAssoc($data_set)) {
384  $data[] = $data_rec;
385  }
386 
387  return $data;
388  }
389 
390  public function getTrackingDataAgg(int $a_user_id): array
391  {
392  global $DIC;
393  $ilDB = $DIC->database();
394 
395  // get all users with any tracking data
396  $sco_set = $ilDB->queryF(
397  '
398  SELECT DISTINCT sco_id FROM scorm_tracking
399  WHERE obj_id = %s
400  AND user_id = %s
401  AND sco_id <> %s',
402  array('integer','integer','integer'),
403  array($this->getId(),$a_user_id,0)
404  );
405 
406  $data = array();
407  while ($sco_rec = $ilDB->fetchAssoc($sco_set)) {
408  $data_set = $ilDB->queryF(
409  '
410  SELECT * FROM scorm_tracking
411  WHERE obj_id = %s
412  AND sco_id = %s
413  AND user_id = %s
414  AND lvalue <> %s
415  AND (lvalue = %s
416  OR lvalue = %s
417  OR lvalue = %s)',
418  array('integer','integer','integer','text','text','text','text'),
419  array($this->getId(),
420  $sco_rec["sco_id"],
421  $a_user_id,
422  "package_attempts",
423  "cmi.core.lesson_status",
424  "cmi.core.total_time",
425  "cmi.core.score.raw")
426  );
427 
428  $score = $time = $status = "";
429 
430  while ($data_rec = $ilDB->fetchAssoc($data_set)) {
431  switch ($data_rec["lvalue"]) {
432  case "cmi.core.lesson_status":
433  $status = $data_rec["rvalue"];
434  break;
435 
436  case "cmi.core.total_time":
437  $time = $data_rec["rvalue"];
438  break;
439 
440  case "cmi.core.score.raw":
441  $score = $data_rec["rvalue"];
442  break;
443  }
444  }
445  $sc_item = new ilSCORMItem((int) $sco_rec["sco_id"]);
446  $data[] = array("sco_id" => (int) $sco_rec["sco_id"], "title" => $sc_item->getTitle(),
447  "score" => $score, "time" => $time, "status" => $status);
448  }
449  return (array) $data;
450  }
451 
452  public function getTrackingDataAggSco(int $a_sco_id): array
453  {
454  global $DIC;
455  $ilDB = $DIC->database();
456 
457  // get all users with any tracking data
458  $user_set = $ilDB->queryF(
459  '
460  SELECT DISTINCT user_id FROM scorm_tracking
461  WHERE obj_id = %s
462  AND sco_id = %s',
463  array('integer','integer'),
464  array($this->getId(),$a_sco_id)
465  );
466 
467  $data = array();
468  while ($user_rec = $ilDB->fetchAssoc($user_set)) {
469  $data_set = $ilDB->queryF(
470  '
471  SELECT * FROM scorm_tracking
472  WHERE obj_id = %s
473  AND sco_id = %s
474  AND user_id = %s
475  AND (lvalue = %s
476  OR lvalue = %s
477  OR lvalue = %s)',
478  array('integer','integer','integer','text','text','text'),
479  array($this->getId(),
480  $a_sco_id,
481  $user_rec["user_id"],
482  "cmi.core.lesson_status",
483  "cmi.core.total_time",
484  "cmi.core.score.raw")
485  );
486 
487  $score = $time = $status = "";
488 
489  while ($data_rec = $ilDB->fetchAssoc($data_set)) {
490  switch ($data_rec["lvalue"]) {
491  case "cmi.core.lesson_status":
492  $status = $data_rec["rvalue"];
493  break;
494 
495  case "cmi.core.total_time":
496  $time = $data_rec["rvalue"];
497  break;
498 
499  case "cmi.core.score.raw":
500  $score = $data_rec["rvalue"];
501  break;
502  }
503  }
504 
505  $data[] = array("user_id" => $user_rec["user_id"],
506  "score" => $score, "time" => $time, "status" => $status);
507  }
508 
509  return $data;
510  }
511 
516  public function exportSelected(bool $a_all, array $a_users = array()): void
517  {
518  global $DIC;
519  $ilDB = $DIC->database();
520  $ilUser = $DIC->user();
521  $privacy = ilPrivacySettings::getInstance();
522  $allowExportPrivacy = $privacy->enabledExportSCORM();
523 
524  $csv = "";
525  $query = 'SELECT * FROM sahs_user WHERE obj_id = %s';
526  if (count($a_users) > 0) {
527  $query .= ' AND ' . $ilDB->in('user_id', $a_users, false, 'integer');
528  }
529  $res = $ilDB->queryF(
530  $query,
531  array('integer'),
532  array($this->getId())
533  );
534  while ($data = $ilDB->fetchAssoc($res)) {
535  $csv = $csv . $data["obj_id"]
536  . ";\"" . $this->getTitle() . "\""
537  . ";" . $data["module_version"]
538  . ";\"" . implode("\";\"", ilSCORMTrackingItems::userDataArrayForExport((int) $data["user_id"], $allowExportPrivacy)) . "\""
539  . ";\"" . $data["last_access"] . "\""
540  . ";\"" . ilLearningProgressBaseGUI::__readStatus((int) $data["obj_id"], (int) $data["user_id"]) . "\"" //not $data["status"] because modifications to learning progress could have made before export
541  . ";" . $data["package_attempts"]
542  . ";" . $data["percentage_completed"]
543  . ";" . $data["sco_total_time_sec"]
544 // . ";\"" . $certificateDate ."\""
545  . "\n";
546  }
548  $header = "LearningModuleId;LearningModuleTitle;LearningModuleVersion;" . str_replace(',', ';', $udh["cols"]) . ";"
549  . "LastAccess;Status;Attempts;percentageCompletedSCOs;SumTotal_timeSeconds\n";
550 
551  $this->sendExportFile($header, $csv);
552  }
553 
554  public function importTrackingData(string $a_file): bool
555  {
556  global $DIC;
557  $ilDB = $DIC->database();
558  $ilUser = $DIC->user();
559 
560  $success = false;
561  //echo file_get_contents($a_file);
562  $method = null;
563 
564  //lets import
565  $fhandle = fopen($a_file, "r");
566 
567  //the top line is the field names
568  $fields = fgetcsv($fhandle, 2 ** 16, ';', '"', '\\');
569  //lets check the import method
570  fclose($fhandle);
571 
572  switch ($fields[0]) {
573  case "Scoid":
574  case "SCO-Identifier":
575  $success = $this->importRaw($a_file);
576  break;
577  case "Department":
578  case "LearningModuleId":
579  $success = $this->importSuccess($a_file);
580  break;
581  default:
582  return false;
583  }
584  return $success;
585  }
586 
587  public function importSuccess(string $a_file): bool
588  {
589  global $DIC;
590  $ilDB = $DIC->database();
591  $ilUser = $DIC->user();
592  $scos = array();
593  $olp = ilObjectLP::getInstance($this->getId());
594  $collection = $olp->getCollectionInstance();
595  if ($collection) {
596  $scos = $collection->getItems();
597  }
598 
599  $fhandle = fopen($a_file, "r");
600 
601  $obj_id = $this->getID();
602  $fields = fgetcsv($fhandle, 2 ** 16, ';', '"', '\\');
603  $users = array();
604  $usersToDelete = array();
605  while (($csv_rows = fgetcsv($fhandle, 2 ** 16, ";", '"', '\\')) !== false) {
606  $user_id = 0;
607  $data = array_combine($fields, $csv_rows);
608  //no check the format - sufficient to import users
609  if (isset($data["Login"])) {
610  $user_id = $this->get_user_id($data["Login"]);
611  }
612  if (isset($data["login"])) {
613  $user_id = $this->get_user_id($data["login"]);
614  }
615  //add mail in future
616  if (isset($data["user"]) && is_numeric($data["user"])) {
617  $user_id = (int) $data["user"];
618  }
619 
620  if ($user_id > 0) {
621  $last_access = new DateTimeImmutable('now');
622  if (isset($data['LastAccess']) && $data['LastAccess']) {
623  $last_access = $this->kindlyToDateTime('Y-m-d H:i:s', $data['LastAccess']);
624  } elseif (isset($data['Date']) && $data['Date']) {
625  $last_access = $this->kindlyToDateTime('d.m.Y', $data['Date']);
626  }
627 
629 
630  if (isset($data["Status"])) {
631  if (is_int($data["Status"])) {
632  $status = $data["Status"];
633  } elseif ($data["Status"] == "0" || $data["Status"] == "1" || $data["Status"] == "2" || $data["Status"] == "3") {
634  $status = (int) $data["Status"];
635  } elseif ($data["Status"] == ilLPStatus::LP_STATUS_NOT_ATTEMPTED) {
637  } elseif ($data["Status"] == ilLPStatus::LP_STATUS_IN_PROGRESS) {
639  } elseif ($data["Status"] == ilLPStatus::LP_STATUS_FAILED) {
641  }
642  }
643 
644  $attempts = null;
645  if (isset($data["Attempts"])) {
646  $attempts = (int) $data["Attempts"];
647  }
648 
649  $percentage_completed = 0;
650  if ($status == ilLPStatus::LP_STATUS_COMPLETED_NUM) {
651  $percentage_completed = 100;
652  }
653  if (isset($data['percentageCompletedSCOs'])) {
654  $percentage_completed = (int) $data['percentageCompletedSCOs'];
655  }
656 
657  $sco_total_time_sec = null;
658  if (isset($data['SumTotal_timeSeconds'])) {
659  $sco_total_time_sec = (int) $data['SumTotal_timeSeconds'];
660  }
661 
663  $usersToDelete[] = $user_id;
664  } else {
665  $this->importSuccessForSahsUser($user_id, $last_access, $status, $attempts, $percentage_completed, $sco_total_time_sec);
666  $users[] = $user_id;
667  }
668 
669  if ($status == ilLPStatus::LP_STATUS_COMPLETED_NUM) {
670  foreach ($scos as $sco_id) {
671  $statement = $ilDB->queryF(
672  '
673  SELECT * FROM scorm_tracking
674  WHERE user_id = %s
675  AND sco_id = %s
676  AND lvalue = %s
677  AND obj_id = %s',
678  array('integer','integer','text','integer'),
679  array($user_id, $sco_id, 'cmi.core.lesson_status',$obj_id)
680  );
681  if ($ilDB->numRows($statement) > 0) {
682  $ilDB->update(
683  'scorm_tracking',
684  array(
685  'rvalue' => array('clob', 'completed'),
686  'c_timestamp' => array('timestamp', $last_access?->format('Y-m-d H:i:s'))
687  ),
688  array(
689  'user_id' => array('integer', $user_id),
690  'sco_id' => array('integer', $sco_id),
691  'lvalue' => array('text', 'cmi.core.lesson_status'),
692  'obj_id' => array('integer', $obj_id)
693  )
694  );
695  } else {
696  $ilDB->insert('scorm_tracking', array(
697  'obj_id' => array('integer', $obj_id),
698  'user_id' => array('integer', $user_id),
699  'sco_id' => array('integer', $sco_id),
700  'lvalue' => array('text', 'cmi.core.lesson_status'),
701  'rvalue' => array('clob', 'completed'),
702  'c_timestamp' => array('timestamp', $last_access?->format('Y-m-d H:i:s'))
703  ));
704  }
705  }
706  }
707  } else {
708  //echo "Warning! User $csv_rows[0] does not exist in ILIAS. Data for this user was skipped.\n";
709  }
710  }
711 
712  if (count($usersToDelete) > 0) {
713  // include_once("../components/ILIAS/Tracking/classes/class.ilLPMarks.php");
714  // ilLPMarks::_deleteForUsers($this->getId(), $usersToDelete);
715  $this->deleteTrackingDataOfUsers($usersToDelete);
716  }
717  ilLPStatusWrapper::_refreshStatus($this->getId(), $users);
718  return true;
719  }
720 
721  protected function kindlyToDateTime(
722  string $format,
723  string $maybe_datetime,
724  ?DateTimeImmutable $default = null
725  ): ?DateTimeImmutable {
726  $datetime = $default;
727 
728  if ($maybe_datetime === '0000-00-00 00:00:00' || $maybe_datetime === '0000-00-00') {
729  return null;
730  }
731 
732  try {
733  $parsed_date = DateTimeImmutable::createFromFormat($format, $maybe_datetime);
734  if ($parsed_date !== false) {
735  $datetime = $parsed_date;
736  }
737  } catch (Throwable) {
738  // Ignore
739  }
740 
741  return $datetime;
742  }
743 
744  public function importSuccessForSahsUser(
745  int $user_id,
746  ?DateTimeImmutable $last_access,
747  int $status,
748  ?int $attempts = null,
749  ?int $percentage_completed = null,
750  ?int $sco_total_time_sec = null
751  ): void {
752  global $DIC;
753  $ilDB = $DIC->database();
754  $statement = $ilDB->queryF(
755  'SELECT * FROM sahs_user WHERE obj_id = %s AND user_id = %s',
756  array('integer','integer'),
757  array($this->getID(),$user_id)
758  );
759  if ($ilDB->numRows($statement) > 0) {
760  $ilDB->update(
761  'sahs_user',
762  array(
763  'last_access' => array('timestamp', $last_access?->format('Y-m-d H:i:s')),
764  'status' => array('integer', $status),
765  'package_attempts' => array('integer', $attempts),
766  'percentage_completed' => array('integer', $percentage_completed),
767  'sco_total_time_sec' => array('integer', $sco_total_time_sec)
768  ),
769  array(
770  'obj_id' => array('integer', $this->getID()),
771  'user_id' => array('integer', $user_id)
772  )
773  );
774  } else {
775  $ilDB->insert('sahs_user', array(
776  'obj_id' => array('integer', $this->getID()),
777  'user_id' => array('integer', $user_id),
778  'last_access' => array('timestamp', $last_access?->format('Y-m-d H:i:s')),
779  'status' => array('integer', $status),
780  'package_attempts' => array('integer', $attempts),
781  'percentage_completed' => array('integer', $percentage_completed),
782  'sco_total_time_sec' => array('integer', $sco_total_time_sec)
783  ));
784  }
785  // since 8 necessary because attempts can be null
786  if (ilChangeEvent::_lookupReadEvents($this->getID(), $user_id) == []) {
787  ilChangeEvent::_recordReadEvent("sahs", $DIC->http()->wrapper()->query()->retrieve('ref_id', $DIC->refinery()->kindlyTo()->int()), $this->getID(), $user_id, false);
788  }
789 
790  ilChangeEvent::_recordReadEvent("sahs", $DIC->http()->wrapper()->query()->retrieve('ref_id', $DIC->refinery()->kindlyTo()->int()), $this->getID(), $user_id, false, $attempts, $sco_total_time_sec);
791  }
792 
796  private function parseUserId(string $il_id): int
797  {
798  global $DIC;
799  $ilSetting = $DIC->settings();
800 
801  $parts = explode('_', $il_id);
802 
803  if (!count((array) $parts)) {
804  return 0;
805  }
806  if (!isset($parts[2]) or !isset($parts[3])) {
807  return 0;
808  }
809  if ($parts[2] != $ilSetting->get('inst_id', $parts[2])) {
810  return 0;
811  }
812  return (int) $parts[3];
813  }
814 
818  private function importRaw(string $a_file): bool
819  {
820  global $DIC;
821  $ilDB = $DIC->database();
822  $ilUser = $DIC->user();
823  $lng = $DIC->language();
824  $lng->loadLanguageModule("scormtrac");
825 
826  $fhandle = fopen($a_file, "r");
827 
828  $fields = fgetcsv($fhandle, 2 ** 16, ';', '"', '\\');
829  $users = array();
830  $a_last_access = array();
831  $a_time = array();
832  $a_package_attempts = array();
833  $a_module_version = array();
834  while (($csv_rows = fgetcsv($fhandle, 2 ** 16, ";", '"', '\\')) !== false) {
835  $data = array_combine($fields, $csv_rows);
836  if ($data['Userid']) {
837  $user_id = $this->parseUserId($data['Userid']);
838  } elseif ($data[$lng->txt("user")]) {
839  if (is_int($data[$lng->txt("user")])) {
840  $user_id = $data[$lng->txt("user")];
841  }
842  }
843  if ($data[$lng->txt("login")]) {
844  $user_id = $this->get_user_id($data[$lng->txt("login")]);
845  }
846  if (!$user_id) {
847  continue;
848  }
849 
850  if ($data['Scoid']) {
851  $il_sco_id = $this->lookupSCOId($data['Scoid']);
852  }
853  if ($data[$lng->txt("identifierref")]) {
854  $il_sco_id = $this->lookupSCOId($data[$lng->txt("identifierref")]);
855  }
856  if (!$il_sco_id) {
857  continue;
858  }
859 
860  $c_timestamp = null;
861  foreach ([$lng->txt("c_timestamp"), 'Timestamp'] as $key) {
862  if (!empty($data[$key])) {
863  $c_timestamp = $this->kindlyToDateTime('Y-m-d H:i:s', $data[$key]);
864  break;
865  }
866  }
867 
868  $c_timestamp ??= new DateTimeImmutable('now');
869  if (!isset($a_last_access[$user_id]) || $a_last_access[$user_id] < $c_timestamp) {
870  $a_last_access[$user_id] = $c_timestamp;
871  }
872 
873  if (!$data['Key']) {
874  continue;
875  }
876  if (!$data['Value']) {
877  $data['Value'] = "";
878  }
879 
880  if ($data['Key'] === "cmi.core.total_time" && $data['Value'] != "") {
881  $tarr = explode(":", $data['Value']);
882  $sec = (int) $tarr[2] + (int) $tarr[1] * 60 +
883  (int) substr($tarr[0], strlen($tarr[0]) - 3) * 60 * 60;
884  if ($a_time[$user_id]) {
885  $a_time[$user_id] += $sec;
886  } else {
887  $a_time[$user_id] = $sec;
888  }
889  }
890  //do the actual import
891  if ($il_sco_id > 0) {
892  $statement = $ilDB->queryF(
893  '
894  SELECT * FROM scorm_tracking
895  WHERE user_id = %s
896  AND sco_id = %s
897  AND lvalue = %s
898  AND obj_id = %s',
899  array('integer', 'integer', 'text', 'integer'),
900  array($user_id, $il_sco_id, $data['Key'], $this->getID())
901  );
902  if ($ilDB->numRows($statement) > 0) {
903  $ilDB->update(
904  'scorm_tracking',
905  array(
906  'rvalue' => array('clob', $data['Value']),
907  'c_timestamp' => array('timestamp', $c_timestamp)
908  ),
909  array(
910  'user_id' => array('integer', $user_id),
911  'sco_id' => array('integer', $il_sco_id),
912  'lvalue' => array('text', $data['Key']),
913  'obj_id' => array('integer', $this->getId())
914  )
915  );
916  } else {
917  $ilDB->insert('scorm_tracking', array(
918  'obj_id' => array('integer', $this->getId()),
919  'user_id' => array('integer', $user_id),
920  'sco_id' => array('integer', $il_sco_id),
921  'lvalue' => array('text', $data['Key']),
922  'rvalue' => array('clob', $data['Value']),
923  'c_timestamp' => array('timestamp', $data['Timestamp'])
924  ));
925  }
926  }
927  // $package_attempts = 1;
928  if ($il_sco_id == 0) {
929  if ($data['Key'] === "package_attempts") {
930  $a_package_attempts[$user_id] = $data['Value'];
931  }
932  // if ($data['Key'] == "module_version") $a_module_version[$user_id] = $data['Value'];
933  }
934  if (!in_array($user_id, $users)) {
935  $users[] = $user_id;
936  }
937  }
938  fclose($fhandle);
939  ilLPStatusWrapper::_refreshStatus($this->getId(), $users);
940  foreach ($users as $user_id) {
941  $attempts = 1;
942  if ($a_package_attempts[$user_id]) {
943  $attempts = $a_package_attempts[$user_id];
944  }
945  // $module_version = 1;
946  // if ($a_module_version[$user_id]) $module_version = $a_module_version[$user_id];
947  $sco_total_time_sec = null;
948  if ($a_time[$user_id]) {
949  $sco_total_time_sec = $a_time[$user_id];
950  }
951  $last_access = null;
952  if (isset($a_last_access[$user_id])) {
953  $last_access = $a_last_access[$user_id];
954  }
955  // $status = ilLPStatusWrapper::_determineStatus($this->getId(),$user_id);
956  $status = (int) ilLPStatus::_lookupStatus($this->getId(), $user_id);
957  // $percentage_completed = ilLPStatusSCORM::determinePercentage($this->getId(),$user_id);
958  $percentage_completed = ilLPStatus::_lookupPercentage($this->getId(), $user_id);
959 
960  $this->importSuccessForSahsUser($user_id, $last_access, $status, $attempts, $percentage_completed, $sco_total_time_sec);
961  }
962 
963  return true;
964  }
965 
966 
967 
968  //helper function
969 
970  public function get_user_id(string $a_login): ?int
971  {
972  global $DIC;
973  $ilDB = $DIC->database();
974  $ilUser = $DIC->user();
975 
976  $val_set = $ilDB->queryF(
977  'SELECT * FROM usr_data WHERE(login=%s)',
978  array('text'),
979  array($a_login)
980  );
981  $val_rec = $ilDB->fetchAssoc($val_set);
982 
983  if (count($val_rec) > 0) {
984  return (int) $val_rec['usr_id'];
985  }
986 
987  return null;
988  }
989 
993  private function lookupSCOId(string $a_referrer): int
994  {
995  global $DIC;
996  $ilDB = $DIC->database();
997  $ilUser = $DIC->user();
998 
999  //non specific SCO entries
1000  if ($a_referrer == "0") {
1001  return 0;
1002  }
1003 
1004  $val_set = $ilDB->queryF(
1005  '
1006  SELECT obj_id FROM sc_item,scorm_tree
1007  WHERE (obj_id = child
1008  AND identifierref = %s
1009  AND slm_id = %s)',
1010  array('text','integer'),
1011  array($a_referrer,$this->getID())
1012  );
1013  $val_rec = $ilDB->fetchAssoc($val_set);
1014 
1015  return (int) $val_rec["obj_id"];
1016  }
1017 
1021  public function getUserIdEmail(string $a_mail): int
1022  {
1023  global $DIC;
1024  $ilDB = $DIC->database();
1025  $ilUser = $DIC->user();
1026 
1027  $val_set = $ilDB->queryF(
1028  'SELECT usr_id FROM usr_data WHERE(email=%s)',
1029  array('text'),
1030  array($a_mail)
1031  );
1032  $val_rec = $ilDB->fetchAssoc($val_set);
1033 
1034 
1035  return (int) $val_rec["usr_id"];
1036  }
1037 
1038  //todo
1042  public function sendExportFile(string $a_header, string $a_content): void
1043  {
1044  $timestamp = time();
1045  $refid = $this->getRefId();
1046  $filename = "scorm_tracking_" . $refid . "_" . $timestamp . ".csv";
1047  ilUtil::deliverData($a_header . $a_content, $filename);
1048  exit;
1049  }
1050 
1056  public static function _getAllScoIds(int $a_id): array
1057  {
1058  global $DIC;
1059  $ilDB = $DIC->database();
1060 
1061  $scos = array();
1062 
1063  $val_set = $ilDB->queryF(
1064  '
1065  SELECT scorm_object.obj_id,
1066  scorm_object.title,
1067  scorm_object.c_type,
1068  scorm_object.slm_id,
1069  scorm_object.obj_id scoid
1070  FROM scorm_object,sc_item,sc_resource
1071  WHERE(scorm_object.slm_id = %s
1072  AND scorm_object.obj_id = sc_item.obj_id
1073  AND sc_item.identifierref = sc_resource.import_id
1074  AND sc_resource.scormtype = %s)
1075  GROUP BY scorm_object.obj_id,
1076  scorm_object.title,
1077  scorm_object.c_type,
1078  scorm_object.slm_id,
1079  scorm_object.obj_id ',
1080  array('integer', 'text'),
1081  array($a_id,'sco')
1082  );
1083 
1084  while ($val_rec = $ilDB->fetchAssoc($val_set)) {
1085  $scos[] = $val_rec['scoid'];
1086  }
1087  return $scos;
1088  }
1089 
1098  public static function _getStatusForUser(int $a_id, int $a_user, array $a_allScoIds, bool $a_numerical = false): bool
1099  {
1100  global $DIC;
1101  $ilDB = $DIC->database();
1102  $lng = $DIC->language();
1103 
1104  $scos = $a_allScoIds;
1105  //check if all SCO's are completed
1106  $scos_c = implode(',', $scos);
1107 
1108  $val_set = $ilDB->queryF(
1109  '
1110  SELECT * FROM scorm_tracking
1111  WHERE (user_id = %s
1112  AND obj_id = %s
1113  AND ' . $ilDB->in('sco_id', $scos, false, 'integer') . '
1114  AND ((lvalue = %s AND ' . $ilDB->like('rvalue', 'clob', 'completed') . ')
1115  OR (lvalue = %s AND ' . $ilDB->like('rvalue', 'clob', 'passed') . ')))',
1116  array('integer','integer','text','text'),
1117  array($a_user,$a_id,'cmi.core.lesson_status', 'cmi.core.lesson_status')
1118  );
1119  while ($val_rec = $ilDB->fetchAssoc($val_set)) {
1120  $key = array_search($val_rec['sco_id'], $scos);
1121  unset($scos[$key]);
1122  }
1123  $completion = false;
1124  //check for completion
1125  if (count($scos) == 0) {
1126  $completion = ($a_numerical === true) ? true : $lng->txt("cont_complete");
1127  }
1128  if (count($scos) > 0) {
1129  $completion = ($a_numerical === true) ? false : $lng->txt("cont_incomplete");
1130  }
1131  return $completion;
1132  }
1133 
1140  public static function _getCourseCompletionForUser(int $a_id, int $a_user): bool
1141  {
1143  }
1144 
1148  public function getAllScoIds(): array
1149  {
1150  global $DIC;
1151  $ilDB = $DIC->database();
1152 
1153  $scos = array();
1154  //get all SCO's of this object
1155  $val_set = $ilDB->queryF(
1156  '
1157  SELECT scorm_object.obj_id,
1158  scorm_object.title,
1159  scorm_object.c_type,
1160  scorm_object.slm_id,
1161  scorm_object.obj_id scoid
1162  FROM scorm_object, sc_item,sc_resource
1163  WHERE(scorm_object.slm_id = %s
1164  AND scorm_object.obj_id = sc_item.obj_id
1165  AND sc_item.identifierref = sc_resource.import_id
1166  AND sc_resource.scormtype = %s )
1167  GROUP BY scorm_object.obj_id,
1168  scorm_object.title,
1169  scorm_object.c_type,
1170  scorm_object.slm_id,
1171  scorm_object.obj_id',
1172  array('integer','text'),
1173  array($this->getId(),'sco')
1174  );
1175 
1176  while ($val_rec = $ilDB->fetchAssoc($val_set)) {
1177  $scos[] = $val_rec['scoid'];
1178  }
1179  return $scos;
1180  }
1181 
1182  public function getStatusForUser(int $a_user, array $a_allScoIds, bool $a_numerical = false): bool
1183  {
1184  global $DIC;
1185  $ilDB = $DIC->database();
1186  $scos = $a_allScoIds;
1187  //loook up status
1188  //check if all SCO's are completed
1189  $scos_c = implode(',', $scos);
1190 
1191  $val_set = $ilDB->queryF(
1192  '
1193  SELECT sco_id FROM scorm_tracking
1194  WHERE (user_id = %s
1195  AND obj_id = %s
1196  AND ' . $ilDB->in('sco_id', $scos, false, 'integer') . '
1197  AND ((lvalue = %s AND ' . $ilDB->like('rvalue', 'clob', 'completed') . ') OR (lvalue = %s AND ' . $ilDB->like('rvalue', 'clob', 'passed') . ') ) )',
1198  array('integer','integer','text','text',),
1199  array($a_user,$this->getID(),'cmi.core.lesson_status','cmi.core.lesson_status')
1200  );
1201  while ($val_rec = $ilDB->fetchAssoc($val_set)) {
1202  $key = array_search($val_rec['sco_id'], $scos);
1203  unset($scos[$key]);
1204  }
1205  //check for completion
1206  if (count($scos) == 0) {
1207  $completion = ($a_numerical === true) ? true : $this->lng->txt("cont_complete");
1208  }
1209  if (count($scos) > 0) {
1210  $completion = ($a_numerical === true) ? false : $this->lng->txt("cont_incomplete");
1211  }
1212  return $completion;
1213  }
1214 
1215  public function getCourseCompletionForUser(int $a_user): bool
1216  {
1217  return $this->getStatusForUser($a_user, $this->getAllScoIds(), true);
1218  }
1219 
1223  public static function _removeTrackingDataForUser(int $user_id): void
1224  {
1225  global $DIC;
1226  $ilDB = $DIC->database();
1227  //gobjective
1228  $ilDB->manipulateF(
1229  'DELETE FROM scorm_tracking WHERE user_id = %s',
1230  array('integer'),
1231  array($user_id)
1232  );
1233  $ilDB->manipulateF(
1234  'DELETE FROM sahs_user WHERE user_id = %s',
1235  array('integer'),
1236  array($user_id)
1237  );
1238  }
1239 
1240  public static function _getScoresForUser(int $a_item_id, int $a_user_id): array
1241  {
1242  global $DIC;
1243  $ilDB = $DIC->database();
1244 
1245  $retAr = array("raw" => null, "max" => null, "scaled" => null);
1246  $val_set = $ilDB->queryF(
1247  "
1248  SELECT lvalue, rvalue FROM scorm_tracking
1249  WHERE sco_id = %s
1250  AND user_id = %s
1251  AND (lvalue = 'cmi.core.score.raw' OR lvalue = 'cmi.core.score.max')",
1252  array('integer', 'integer'),
1253  array($a_item_id, $a_user_id)
1254  );
1255  while ($val_rec = $ilDB->fetchAssoc($val_set)) {
1256  if ($val_rec['lvalue'] === "cmi.core.score.raw") {
1257  $retAr["raw"] = $val_rec["rvalue"];
1258  }
1259  if ($val_rec['lvalue'] === "cmi.core.score.max") {
1260  $retAr["max"] = $val_rec["rvalue"];
1261  }
1262  }
1263  if ($retAr["raw"] != null && $retAr["max"] != null) {
1264  $retAr["scaled"] = ($retAr["raw"] / $retAr["max"]);
1265  }
1266 
1267  return $retAr;
1268  }
1269 
1270  public function getLastVisited(int $user_id): string
1271  {
1272  global $DIC;
1273  $ilDB = $DIC->database();
1274  $val_set = $ilDB->queryF(
1275  'SELECT last_visited FROM sahs_user WHERE obj_id = %s AND user_id = %s',
1276  array('integer','integer'),
1277  array($this->getID(),$user_id)
1278  );
1279  while ($val_rec = $ilDB->fetchAssoc($val_set)) {
1280  if ($val_rec["last_visited"] != null) {
1281  return "" . $val_rec["last_visited"];
1282  }
1283  }
1284  return '0';
1285  }
1286 
1287  public function deleteTrackingDataOfUsers(array $a_users): void
1288  {
1289  global $DIC;
1290  $ilDB = $DIC->database();
1291 
1292  ilChangeEvent::_deleteReadEventsForUsers($this->getId(), $a_users);
1293 
1294  foreach ($a_users as $usr) {
1295  $user = (int) $usr;
1296  $ilDB->manipulateF(
1297  '
1298  DELETE FROM scorm_tracking
1299  WHERE user_id = %s
1300  AND obj_id = %s',
1301  array('integer', 'integer'),
1302  array($user, $this->getID())
1303  );
1304 
1305  $ilDB->manipulateF(
1306  '
1307  DELETE FROM sahs_user
1308  WHERE user_id = %s
1309  AND obj_id = %s',
1310  array('integer', 'integer'),
1311  array($user, $this->getID())
1312  );
1313 
1315  }
1316  }
1317 }
const LP_STATUS_COMPLETED_NUM
static _getStatusForUser(int $a_id, int $a_user, array $a_allScoIds, bool $a_numerical=false)
Get the status of a SCORM module for a given user.
$res
Definition: ltiservices.php:66
static _getCourseCompletionForUser(int $a_id, int $a_user)
Get the completion of a SCORM module for a given user.
getNodeData(int $a_node_id, ?int $a_tree_pk=null)
get all information of a node.
getUserIdEmail(string $a_mail)
assumes that only one account exists for a mailadress
getAttemptsForUsers()
Get attempts for all users.
__construct(int $a_id=0, bool $a_call_by_reference=true)
Constructor.
txt(string $a_topic, string $a_default_lang_fallback_mod="")
gets the text for a given topic if the topic is not in the list, the topic itself with "-" will be re...
if($clientAssertionType !='urn:ietf:params:oauth:client-assertion-type:jwt-bearer'|| $grantType !='client_credentials') $parts
Definition: ltitoken.php:61
kindlyToDateTime(string $format, string $maybe_datetime, ?DateTimeImmutable $default=null)
const LP_STATUS_NOT_ATTEMPTED
static _removeTrackingDataForUser(int $user_id)
to be called from IlObjUser
getTrackedItems()
get all tracked items of current user
$datetime
static _lookupPercentage(int $a_obj_id, int $a_user_id)
Lookup percentage.
ilTree $tree
const LP_STATUS_IN_PROGRESS_NUM
loadLanguageModule(string $a_module)
Load language module.
static deliverData(string $a_data, string $a_filename, string $mime="application/octet-stream")
static _getTrackingItems(int $a_obj_id)
get all tracking items of scorm object
getModuleVersionForUser(int $a_user_id)
get module version that tracking data for a user was recorded on
$ilErr
Definition: raiseError.php:33
const LP_STATUS_IN_PROGRESS
getStatusForUser(int $a_user, array $a_allScoIds, bool $a_numerical=false)
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
static _getScoresForUser(int $a_item_id, int $a_user_id)
const LP_STATUS_FAILED
lookupSCOId(string $a_referrer)
resolves manifest SCOID to internal ILIAS SCO ID
static userDataArrayForExport(int $user, bool $b_allowExportPrivacy=false)
ilLanguage $lng
static _recordReadEvent(string $a_type, int $a_ref_id, int $obj_id, int $usr_id, bool $isCatchupWriteEvents=true, $a_ext_rc=null, $a_ext_time=null)
global $DIC
Definition: shib_login.php:26
static _refreshStatus(int $a_obj_id, ?array $a_users=null)
SCORM Item.
sendExportFile(string $a_header, string $a_content)
send export file to browser
getTrackingDataPerUser(int $a_sco_id, int $a_user_id)
Get tracking data per user.
SCORM Object Tree.
static _lookupStatus(int $a_obj_id, int $a_user_id, bool $a_create=true)
Lookup status.
static _lookupReadEvents($obj_id, $usr_id=null)
Reads all read events which occured on the object.
importRaw(string $a_file)
Import raw data.
exit
getModuleVersionForUsers()
Get module version for users.
$filename
Definition: buildRTE.php:78
foreach($mandatory_scripts as $file) $timestamp
Definition: buildRTE.php:70
const LP_STATUS_NOT_ATTEMPTED_NUM
getDataDirectory(?string $mode="filesystem")
get data directory of lm
static __readStatus(int $a_obj_id, int $user_id)
PhpInconsistentReturnPointsInspection
$lm_set
exportSelected(bool $a_all, array $a_users=array())
Export selected user tracking data ilObjUser $ilUser.
global $ilSetting
Definition: privfeed.php:31
setLearningProgressSettingsAtUpload()
set settings for learning progress determination per default at upload
__construct(Container $dic, ilPlugin $plugin)
getTrackedUsers(string $a_search)
Return the last access timestamp for a given user.
static _deleteReadEventsForUsers(int $a_obj_id, array $a_user_ids)
importSuccessForSahsUser(int $user_id, ?DateTimeImmutable $last_access, int $status, ?int $attempts=null, ?int $percentage_completed=null, ?int $sco_total_time_sec=null)
getSubTree(array $a_node, bool $a_with_data=true, array $a_type=[])
get all nodes in the subtree under specified node
getAttemptsForUser(int $a_user_id)
get number of attempts for a certain user and package
static getInstance(int $obj_id)
Class ilObjSCORMLearningModule.
static _getAllScoIds(int $a_id)
Get an array of id&#39;s for all Sco&#39;s in the module.
parseUserId(string $il_id)
Parse il_usr_123_6 id.
Class ilObjSCORMLearningModule.
const LP_STATUS_FAILED_NUM
static _updateStatus(int $a_obj_id, int $a_usr_id, ?object $a_obj=null, bool $a_percentage=false, bool $a_force_raise=false)
ilObjUser $user