ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
class.assQuestion.php
Go to the documentation of this file.
1 <?php
2 
25 use ILIAS\Notes\InternalDataService as NotesInternalDataService;
26 use ILIAS\Notes\NoteDBRepository as NotesRepo;
29 
30 require_once './Modules/Test/classes/inc.AssessmentConstants.php';
31 
46 abstract class assQuestion
47 {
48  protected const HAS_SPECIFIC_FEEDBACK = true;
49 
50  private const DEFAULT_THUMB_SIZE = 150;
51  private const MINIMUM_THUMB_SIZE = 20;
52  private const MAXIMUM_THUMB_SIZE = 8192;
53  public const TRIM_PATTERN = '/^[\p{C}\p{Z}]+|[\p{C}\p{Z}]+$/u';
54 
55  protected \ILIAS\TestQuestionPool\QuestionInfoService $questioninfo;
56  protected \ILIAS\Test\TestParticipantInfoService $testParticipantInfo;
57 
60  protected \ILIAS\TestQuestionPool\QuestionFilesService $questionFilesService;
62 
63  protected int $id;
64  protected string $title;
65  protected string $comment;
66  protected string $owner;
67  protected string $author;
68  protected int $thumb_size;
69 
73  protected string $question;
74 
78  protected float $points;
79 
83  protected bool $shuffle;
84 
88  protected int $test_id;
89 
93  protected int $obj_id = 0;
94 
100  protected $ilias;
101 
103 
104  protected ilLanguage $lng;
105 
106  protected ilDBInterface $db;
107 
108  protected Container $dic;
109 
115  protected array $suggested_solutions;
116 
117  protected ?int $original_id = null;
118 
124  protected $page;
125 
126  private int $nr_of_tries;
127 
131  private string $export_image_path;
132 
133  protected ?string $external_id = null;
134 
135  public const ADDITIONAL_CONTENT_EDITING_MODE_RTE = 'default';
136  public const ADDITIONAL_CONTENT_EDITING_MODE_IPE = 'pageobject';
137 
138  private string $additionalContentEditingMode = '';
139 
140  public \ilAssQuestionFeedback $feedbackOBJ;
141 
142  public bool $prevent_rte_usage = false;
143 
144  public bool $selfassessmenteditingmode = false;
145 
146  public int $defaultnroftries = 0;
147 
148  protected \ilAssQuestionProcessLocker $processLocker;
149 
150  public string $questionActionCmd = 'handleQuestionAction';
151 
155  protected $step;
156 
157  protected $lastChange;
158 
160 
161  private bool $obligationsToBeConsidered = false;
162 
164 
166 
168 
170 
174  public function __construct(
175  string $title = "",
176  string $comment = "",
177  string $author = "",
178  int $owner = -1,
179  string $question = ""
180  ) {
181  global $DIC;
182  $this->dic = $DIC;
183  $lng = $DIC['lng'];
184  $tpl = $DIC['tpl'];
185  $ilDB = $DIC['ilDB'];
186  $ilLog = $DIC->logger();
187  $this->questioninfo = $DIC->testQuestionPool()->questionInfo();
188  $this->questionFilesService = $DIC->testQuestionPool()->questionFiles();
189  $this->testParticipantInfo = $DIC->test()->testParticipantInfo();
190  $this->current_user = $DIC['ilUser'];
191  $this->lng = $lng;
192  $this->tpl = $tpl;
193  $this->db = $ilDB;
194  $this->ilLog = $ilLog;
195  $this->http = $DIC->http();
196  $this->refinery = $DIC->refinery();
197 
198  $this->thumb_size = self::DEFAULT_THUMB_SIZE;
199 
200  $this->title = $title;
201  $this->comment = $comment;
202  $this->setAuthor($author);
203  $this->setOwner($owner);
204 
205  $this->setQuestion($question);
206 
207  $this->id = -1;
208  $this->test_id = -1;
209  $this->suggested_solutions = [];
210  $this->shuffle = 1;
211  $this->nr_of_tries = 0;
212  $this->setExternalId(null);
213 
214  $this->questionActionCmd = 'handleQuestionAction';
215  $this->export_image_path = '';
216  $this->shuffler = $DIC->refinery()->random()->dontShuffle();
217  $this->lifecycle = ilAssQuestionLifecycle::getDraftInstance();
218  $this->skillUsageService = $DIC->skills()->usage();
219  }
220 
221  protected static $forcePassResultsUpdateEnabled = false;
222 
224  {
225  self::$forcePassResultsUpdateEnabled = $forcePassResultsUpdateEnabled;
226  }
227 
228  public static function isForcePassResultUpdateEnabled(): bool
229  {
230  return self::$forcePassResultsUpdateEnabled;
231  }
232 
233  protected function getQuestionAction(): string
234  {
235  if (!isset($_POST['cmd']) || !isset($_POST['cmd'][$this->questionActionCmd])) {
236  return '';
237  }
238 
239  if (!is_array($_POST['cmd'][$this->questionActionCmd]) || $_POST['cmd'][$this->questionActionCmd] === []) {
240  return '';
241  }
242 
243  return key($_POST['cmd'][$this->questionActionCmd]);
244  }
245 
246  protected function isNonEmptyItemListPostSubmission(string $postSubmissionFieldname): bool
247  {
248  if (!isset($_POST[$postSubmissionFieldname])) {
249  return false;
250  }
251 
252  if (!is_array($_POST[$postSubmissionFieldname])) {
253  return false;
254  }
255 
256  if (!count($_POST[$postSubmissionFieldname])) {
257  return false;
258  }
259 
260  return true;
261  }
262 
263  protected function log(int $active_id, string $langVar): void
264  {
266  $message = $this->lng->txtlng('assessment', $langVar, ilObjAssessmentFolder::_getLogLanguage());
267  assQuestion::logAction($message, $active_id, $this->getId());
268  }
269  }
270 
271  public function getShuffler(): Transformation
272  {
273  return $this->shuffler;
274  }
275 
276  public function setShuffler(Transformation $shuffler): void
277  {
278  $this->shuffler = $shuffler;
279  }
280 
281  public function setProcessLocker(ilAssQuestionProcessLocker $processLocker): void
282  {
283  $this->processLocker = $processLocker;
284  }
285 
287  {
288  return $this->processLocker;
289  }
290 
301  public function fromXML($item, int $questionpool_id, ?int $tst_id, &$tst_object, int &$question_counter, array $import_mapping, array &$solutionhints = []): array
302  {
303  $classname = $this->getQuestionType() . "Import";
304  $import = new $classname($this);
305  $import_mapping = $import->fromXML($item, $questionpool_id, $tst_id, $tst_object, $question_counter, $import_mapping);
306 
307  foreach ($solutionhints as $hint) {
308  $this->importHint($import->getQuestionId(), $hint);
309  }
310  return $import_mapping;
311  }
312 
313  private function importHint(int $question_id, array $hint_array): void
314  {
315  $hint = new ilAssQuestionHint();
316  $hint->setQuestionId($question_id);
317  $hint->setIndex($hint_array['index'] ?? '');
318  $hint->setPoints($hint_array['points'] ?? '');
319  if ($this->getAdditionalContentEditingMode() === self::ADDITIONAL_CONTENT_EDITING_MODE_IPE) {
320  $hint->save();
321  $hint_page = (new ilAssHintPage());
322  $hint_page->setParentId($question_id);
323  $hint_page->setId($hint->getId());
324  $hint_page->setXMLContent($hint_array['txt']);
325  $hint_page->createFromXML();
326  return;
327  }
328 
329  $hint->setText($hint_array['txt'] ?? '');
330  $hint->save();
331  }
332 
338  public function toXML(
339  bool $a_include_header = true,
340  bool $a_include_binary = true,
341  bool $a_shuffle = false,
342  bool $test_output = false,
343  bool $force_image_references = false
344  ): string {
345  $classname = $this->getQuestionType() . "Export";
346  $export = new $classname($this);
347  return $export->toXML($a_include_header, $a_include_binary, $a_shuffle, $test_output, $force_image_references);
348  }
349 
355  abstract public function isComplete(): bool;
356 
357  public function setTitle(string $title = ""): void
358  {
359  $this->title = $title;
360  }
361 
362  public function setId(int $id = -1): void
363  {
364  $this->id = $id;
365  }
366 
367  public function setTestId(int $id = -1): void
368  {
369  $this->test_id = $id;
370  }
371 
372  public function setComment(string $comment = ""): void
373  {
374  $this->comment = $comment;
375  }
376 
377  public function setShuffle(?bool $shuffle = true): void
378  {
379  $this->shuffle = $shuffle ?? false;
380  }
381 
382  public function setAuthor(string $author = ""): void
383  {
384  if ($author === '') {
385  $author = $this->current_user->getFullname();
386  }
387  $this->author = $author;
388  }
389 
390  public function setOwner(int $owner = -1): void
391  {
392  $this->owner = $owner;
393  }
394 
395  public function getTitle(): string
396  {
397  return $this->title;
398  }
399 
400  public function getTitleForHTMLOutput(): string
401  {
402  return htmlspecialchars($this->title);
403  }
404 
405  public function getTitleFilenameCompliant(): string
406  {
407  return ilFileUtils::getASCIIFilename($this->getTitle());
408  }
409 
410  public function getId(): int
411  {
412  return $this->id;
413  }
414 
415  public function getShuffle(): bool
416  {
417  return $this->shuffle;
418  }
419 
420  public function getTestId(): int
421  {
422  return $this->test_id;
423  }
424 
425  public function getComment(): string
426  {
427  return $this->comment;
428  }
429 
430  public function getDescriptionForHTMLOutput(): string
431  {
432  return htmlspecialchars($this->comment);
433  }
434 
435  public function getThumbSize(): int
436  {
437  return $this->thumb_size;
438  }
439 
440  public function setThumbSize(int $a_size): void
441  {
442  if ($a_size >= self::MINIMUM_THUMB_SIZE) {
443  $this->thumb_size = $a_size;
444  } else {
445  throw new ilException("Thumb size must be at least " . self::MINIMUM_THUMB_SIZE . "px");
446  }
447  }
448 
449  public function getMinimumThumbSize(): int
450  {
451  return self::MINIMUM_THUMB_SIZE;
452  }
453 
454  public function getMaximumThumbSize(): int
455  {
456  return self::MAXIMUM_THUMB_SIZE;
457  }
458 
459  public function getAuthor(): string
460  {
461  return $this->author;
462  }
463 
464  public function getAuthorForHTMLOutput(): string
465  {
466  return $this->refinery->string()->stripTags()->transform($this->author);
467  }
468 
469  public function getOwner(): int
470  {
471  return $this->owner;
472  }
473 
474  public function getObjId(): int
475  {
476  return $this->obj_id;
477  }
478 
479  public function setObjId(int $obj_id = 0): void
480  {
481  $this->obj_id = $obj_id;
482  }
483 
485  {
486  return $this->lifecycle;
487  }
488 
489  public function setLifecycle(ilAssQuestionLifecycle $lifecycle): void
490  {
491  $this->lifecycle = $lifecycle;
492  }
493 
494  public function setExternalId(?string $external_id): void
495  {
496  $this->external_id = $external_id;
497  }
498 
499  public function getExternalId(): string
500  {
501  if ($this->external_id === null || $this->external_id === '') {
502  if ($this->getId() > 0) {
503  return 'il_' . IL_INST_ID . '_qst_' . $this->getId();
504  }
505  return uniqid('', true);
506  }
507  return $this->external_id;
508  }
509 
510  public static function _getSuggestedSolutionOutput(int $question_id): string
511  {
512  $question = self::instantiateQuestion($question_id);
513  if (!is_object($question)) {
514  return "";
515  }
516  return $question->getSuggestedSolutionOutput();
517  }
518 
519  public function getSuggestedSolutionOutput(): string
520  {
521  $output = [];
522  foreach ($this->suggested_solutions as $solution) {
523  switch ($solution->getType()) {
524  case assQuestionSuggestedSolution::TYPE_LM:
525  case assQuestionSuggestedSolution::TYPE_LM_CHAPTER:
526  case assQuestionSuggestedSolution::TYPE_LM_PAGE:
527  case assQuestionSuggestedSolution::TYPE_GLOSARY_TERM:
528  $output[] = '<a href="'
529  . assQuestion::_getInternalLinkHref($solution->getInternalLink())
530  . '">'
531  . $this->lng->txt("solution_hint")
532  . '</a>';
533  break;
534 
535  case assQuestionSuggestedSolution::TYPE_FILE:
536  $possible_texts = array_values(
537  array_filter(
538  [
539  ilLegacyFormElementsUtil::prepareFormOutput($solution->getTitle()),
540  ilLegacyFormElementsUtil::prepareFormOutput($solution->getFilename()),
541  $this->lng->txt('tst_show_solution_suggested')
542  ]
543  )
544  );
545 
547  $output[] = '<a href="'
549  $this->getSuggestedSolutionPathWeb() . $solution->getFilename()
550  )
551  . '">'
552  . $possible_texts[0]
553  . '</a>';
554  break;
555  }
556  }
557  return implode("<br />", $output);
558  }
559 
560  public function getSuggestedSolutions(): array
561  {
563  }
564 
565  public static function _getReachedPoints(int $active_id, int $question_id, int $pass): float
566  {
567  global $DIC;
568  $ilDB = $DIC['ilDB'];
569 
570  $points = 0.0;
571 
572  $result = $ilDB->queryF(
573  "SELECT * FROM tst_test_result WHERE active_fi = %s AND question_fi = %s AND pass = %s",
574  array('integer','integer','integer'),
575  array($active_id, $question_id, $pass)
576  );
577  if ($result->numRows() == 1) {
578  $row = $ilDB->fetchAssoc($result);
579  $points = (float) $row["points"];
580  }
581  return $points;
582  }
583 
584  public function getReachedPoints(int $active_id, int $pass): float
585  {
586  return round(self::_getReachedPoints($active_id, $this->getId(), $pass), 2);
587  }
588 
589  public function getMaximumPoints(): float
590  {
591  return $this->points;
592  }
593 
601  final public function getAdjustedReachedPoints(int $active_id, int $pass, bool $authorizedSolution = true): float
602  {
603  // determine reached points for submitted solution
604  $reached_points = $this->calculateReachedPoints($active_id, $pass, $authorizedSolution);
605  $hintTracking = new ilAssQuestionHintTracking($this->getId(), $active_id, $pass);
606  $requestsStatisticData = $hintTracking->getRequestStatisticDataByQuestionAndTestpass();
607  $reached_points = $reached_points - $requestsStatisticData->getRequestsPoints();
608 
609  // adjust reached points regarding to tests scoring options
610  $reached_points = $this->adjustReachedPointsByScoringOptions($reached_points, $active_id, $pass);
611 
612  return $reached_points;
613  }
614 
618  final public function calculateResultsFromSolution(int $active_id, int $pass, bool $obligationsEnabled = false): void
619  {
620  // determine reached points for submitted solution
621  $reached_points = $this->calculateReachedPoints($active_id, $pass);
622  $questionHintTracking = new ilAssQuestionHintTracking($this->getId(), $active_id, $pass);
623  $requestsStatisticData = $questionHintTracking->getRequestStatisticDataByQuestionAndTestpass();
624  $reached_points = $reached_points - $requestsStatisticData->getRequestsPoints();
625 
626  // adjust reached points regarding to tests scoring options
627  $reached_points = $this->adjustReachedPointsByScoringOptions($reached_points, $active_id, $pass);
628 
629  if ($obligationsEnabled && ilObjTest::isQuestionObligatory($this->getId())) {
630  $isAnswered = $this->isAnswered($active_id, $pass);
631  } else {
632  $isAnswered = true;
633  }
634 
635  if (is_null($reached_points)) {
636  $reached_points = 0.0;
637  }
638 
639  // fau: testNav - check for existing authorized solution to know if a result record should be written
640  $existingSolutions = $this->lookupForExistingSolutions($active_id, $pass);
641 
642  $this->getProcessLocker()->executeUserQuestionResultUpdateOperation(
643  function () use ($active_id, $pass, $reached_points, $requestsStatisticData, $isAnswered, $existingSolutions) {
644  $query = "
645  DELETE FROM tst_test_result
646 
647  WHERE active_fi = %s
648  AND question_fi = %s
649  AND pass = %s
650  ";
651 
652  $types = ['integer', 'integer', 'integer'];
653  $values = [$active_id, $this->getId(), $pass];
654 
655  if ($this->getStep() !== null) {
656  $query .= "
657  AND step = %s
658  ";
659 
660  $types[] = 'integer';
661  $values[] = $this->getStep();
662  }
663  $this->db->manipulateF($query, $types, $values);
664 
665  if ($existingSolutions['authorized']) {
666  $next_id = $this->db->nextId("tst_test_result");
667  $fieldData = array(
668  'test_result_id' => array('integer', $next_id),
669  'active_fi' => array('integer', $active_id),
670  'question_fi' => array('integer', $this->getId()),
671  'pass' => array('integer', $pass),
672  'points' => array('float', $reached_points),
673  'tstamp' => array('integer', time()),
674  'hint_count' => array('integer', $requestsStatisticData->getRequestsCount()),
675  'hint_points' => array('float', $requestsStatisticData->getRequestsPoints()),
676  'answered' => array('integer', $isAnswered)
677  );
678 
679  if ($this->getStep() !== null) {
680  $fieldData['step'] = array('integer', $this->getStep());
681  }
682 
683  $this->db->insert('tst_test_result', $fieldData);
684  }
685  }
686  );
687 
690  sprintf(
691  $this->lng->txtlng(
692  "assessment",
693  "log_user_answered_question",
695  ),
696  $reached_points
697  ),
698  $active_id,
699  $this->getId()
700  );
701  }
702 
703  // update test pass results
704  $test = new ilObjTest(
705  $this->getObjId(),
706  false
707  );
708  $test->updateTestPassResults($active_id, $pass, $obligationsEnabled, $this->getProcessLocker());
709  ilCourseObjectiveResult::_updateObjectiveResult($this->current_user->getId(), $active_id, $this->getId());
710  }
711 
716  final public function persistWorkingState(int $active_id, $pass, bool $obligationsEnabled = false, bool $authorized = true): bool
717  {
718  if (!$this instanceof ilAssQuestionPartiallySaveable && !$this->validateSolutionSubmit()) {
719  return false;
720  }
721 
722  $saveStatus = false;
723 
724  $this->getProcessLocker()->executePersistWorkingStateLockOperation(function () use ($active_id, $pass, $authorized, $obligationsEnabled, &$saveStatus) {
725  if ($pass === null) {
726  $pass = ilObjTest::_getPass($active_id);
727  }
728 
729  $saveStatus = $this->saveWorkingData($active_id, $pass, $authorized);
730 
731  if ($authorized) {
732  // fau: testNav - remove an intermediate solution if the authorized solution is saved
733  // the intermediate solution would set the displayed question status as "editing ..."
734  $this->removeIntermediateSolution($active_id, $pass);
735  // fau.
736  $this->calculateResultsFromSolution($active_id, $pass, $obligationsEnabled);
737  }
738  });
739 
740  return $saveStatus;
741  }
742 
746  final public function persistPreviewState(ilAssQuestionPreviewSession $previewSession): bool
747  {
748  $this->savePreviewData($previewSession);
749  return $this->validateSolutionSubmit();
750  }
751 
752  public function validateSolutionSubmit(): bool
753  {
754  return true;
755  }
756 
765  abstract public function saveWorkingData(int $active_id, int $pass, bool $authorized = true): bool;
766 
767  protected function savePreviewData(ilAssQuestionPreviewSession $previewSession): void
768  {
769  $previewSession->setParticipantsSolution($this->getSolutionSubmit());
770  }
771 
772  public static function logAction(string $logtext, int $active_id, int $question_id): void
773  {
774  global $DIC;
775  $original_id = $DIC->testQuestionPool()->questionInfo()->getOriginalId($question_id);
776 
778  $DIC->user()->getId(),
780  $logtext,
781  $question_id,
783  );
784  }
785 
786  public function getSuggestedSolutionPath(): string
787  {
788  return CLIENT_WEB_DIR . "/assessment/$this->obj_id/$this->id/solution/";
789  }
790 
795  public function getImagePath($question_id = null, $object_id = null): string
796  {
797  if ($question_id === null) {
798  $question_id = $this->id;
799  }
800 
801  if ($object_id === null) {
802  $object_id = $this->obj_id;
803  }
804 
805  return $this->questionFilesService->buildImagePath($question_id, $object_id);
806  }
807 
808  public function getSuggestedSolutionPathWeb(): string
809  {
810  $webdir = ilFileUtils::removeTrailingPathSeparators(CLIENT_WEB_DIR) . "/assessment/$this->obj_id/$this->id/solution/";
811  return str_replace(
812  ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH),
814  $webdir
815  );
816  }
817 
823  public function getImagePathWeb(): string
824  {
825  if (!$this->export_image_path) {
826  $webdir = ilFileUtils::removeTrailingPathSeparators(CLIENT_WEB_DIR) . "/assessment/$this->obj_id/$this->id/images/";
827  return str_replace(
828  ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH),
830  $webdir
831  );
832  }
834  }
835 
836  // hey: prevPassSolutions - accept and prefer intermediate only from current pass
837  public function getTestOutputSolutions(int $activeId, int $pass): array
838  {
839  if ($this->getTestPresentationConfig()->isSolutionInitiallyPrefilled()) {
840  return $this->getSolutionValues($activeId, $pass, true);
841  }
842  return $this->getUserSolutionPreferingIntermediate($activeId, $pass);
843  }
844  // hey.
845 
846  public function getUserSolutionPreferingIntermediate(int $active_id, $pass = null): array
847  {
848  $solution = $this->getSolutionValues($active_id, $pass, false);
849 
850  if (!count($solution)) {
851  $solution = $this->getSolutionValues($active_id, $pass, true);
852  }
853 
854  return $solution;
855  }
856 
862  public function getSolutionValues($active_id, $pass = null, bool $authorized = true): array
863  {
864  if ($pass === null && is_numeric($active_id)) {
865  $pass = $this->getSolutionMaxPass((int) $active_id);
866  }
867 
868  if ($this->getStep() !== null) {
869  $query = "
870  SELECT *
871  FROM tst_solutions
872  WHERE active_fi = %s
873  AND question_fi = %s
874  AND pass = %s
875  AND step = %s
876  AND authorized = %s
877  ORDER BY solution_id";
878 
879  $result = $this->db->queryF(
880  $query,
881  array('integer', 'integer', 'integer', 'integer', 'integer'),
882  array((int) $active_id, $this->getId(), $pass, $this->getStep(), (int) $authorized)
883  );
884  } else {
885  $query = "
886  SELECT *
887  FROM tst_solutions
888  WHERE active_fi = %s
889  AND question_fi = %s
890  AND pass = %s
891  AND authorized = %s
892  ORDER BY solution_id
893  ";
894 
895  $result = $this->db->queryF(
896  $query,
897  array('integer', 'integer', 'integer', 'integer'),
898  array((int) $active_id, $this->getId(), $pass, (int) $authorized)
899  );
900  }
901 
902  $values = [];
903 
904  while ($row = $this->db->fetchAssoc($result)) {
905  $values[] = $row;
906  }
907 
908  return $values;
909  }
910 
914  abstract public function getAdditionalTableName();
915 
919  abstract public function getAnswerTableName();
920 
921  public function deleteAnswers(int $question_id): void
922  {
923  $answer_table_name = $this->getAnswerTableName();
924 
925  if (!is_array($answer_table_name)) {
926  $answer_table_name = array($answer_table_name);
927  }
928 
929  foreach ($answer_table_name as $table) {
930  if (strlen($table)) {
931  $this->db->manipulateF(
932  "DELETE FROM $table WHERE question_fi = %s",
933  array('integer'),
934  array($question_id)
935  );
936  }
937  }
938  }
939 
940  public function deleteAdditionalTableData(int $question_id): void
941  {
942  $additional_table_name = $this->getAdditionalTableName();
943 
944  if (!is_array($additional_table_name)) {
945  $additional_table_name = array($additional_table_name);
946  }
947 
948  foreach ($additional_table_name as $table) {
949  if (strlen($table)) {
950  $this->db->manipulateF(
951  "DELETE FROM $table WHERE question_fi = %s",
952  array('integer'),
953  array($question_id)
954  );
955  }
956  }
957  }
958 
959  protected function deletePageOfQuestion(int $question_id): void
960  {
961  if (ilAssQuestionPage::_exists('qpl', $question_id, "", true)) {
962  $page = new ilAssQuestionPage($question_id);
963  $page->delete();
964  }
965  }
966 
967  public function delete(int $question_id): void
968  {
969  if ($question_id < 1) {
970  return;
971  }
972 
973  $result = $this->db->queryF(
974  "SELECT obj_fi FROM qpl_questions WHERE question_id = %s",
975  array('integer'),
976  array($question_id)
977  );
978  if ($this->db->numRows($result) == 1) {
979  $row = $this->db->fetchAssoc($result);
980  $obj_id = $row["obj_fi"];
981  } else {
982  return; // nothing to do
983  }
984  try {
985  $this->deletePageOfQuestion($question_id);
986  } catch (Exception $e) {
987  $this->ilLog->root()->error("EXCEPTION: Could not delete page of question $question_id: $e");
988  return;
989  }
990 
991  $affectedRows = $this->db->manipulateF(
992  "DELETE FROM qpl_questions WHERE question_id = %s",
993  array('integer'),
994  array($question_id)
995  );
996  if ($affectedRows == 0) {
997  return;
998  }
999 
1000  try {
1001  $this->deleteAdditionalTableData($question_id);
1002  $this->deleteAnswers($question_id);
1003  $this->feedbackOBJ->deleteGenericFeedbacks($question_id, $this->isAdditionalContentEditingModePageObject());
1004  $this->feedbackOBJ->deleteSpecificAnswerFeedbacks($question_id, $this->isAdditionalContentEditingModePageObject());
1005  } catch (Exception $e) {
1006  $this->ilLog->root()->error("EXCEPTION: Could not delete additional table data of question $question_id: $e");
1007  }
1008 
1009  try {
1010  // delete the question in the tst_test_question table (list of test questions)
1011  $affectedRows = $this->db->manipulateF(
1012  "DELETE FROM tst_test_question WHERE question_fi = %s",
1013  array('integer'),
1014  array($question_id)
1015  );
1016  } catch (Exception $e) {
1017  $this->ilLog->root()->error("EXCEPTION: Could not delete delete question $question_id from a test: $e");
1018  }
1019 
1020  try {
1021  $this->getSuggestedSolutionsRepo()->deleteForQuestion($question_id);
1022  } catch (Exception $e) {
1023  $this->ilLog->root()->error("EXCEPTION: Could not delete suggested solutions of question $question_id: $e");
1024  }
1025 
1026  try {
1027  $directory = CLIENT_WEB_DIR . "/assessment/" . $obj_id . "/$question_id";
1028  if (preg_match("/\d+/", $obj_id) and preg_match("/\d+/", $question_id) and is_dir($directory)) {
1029  ilFileUtils::delDir($directory);
1030  }
1031  } catch (Exception $e) {
1032  $this->ilLog->root()->error("EXCEPTION: Could not delete question file directory $directory of question $question_id: $e");
1033  }
1034 
1035  try {
1036  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $question_id);
1037  // remaining usages are not in text anymore -> delete them
1038  // and media objects (note: delete method of ilObjMediaObject
1039  // checks whether object is used in another context; if yes,
1040  // the object is not deleted!)
1041  foreach ($mobs as $mob) {
1042  ilObjMediaObject::_removeUsage($mob, "qpl:html", $question_id);
1043  if (ilObjMediaObject::_exists($mob)) {
1044  $mob_obj = new ilObjMediaObject($mob);
1045  $mob_obj->delete();
1046  }
1047  }
1048  } catch (Exception $e) {
1049  $this->ilLog->root()->error("EXCEPTION: Error deleting the media objects of question $question_id: $e");
1050  }
1051  ilAssQuestionHintTracking::deleteRequestsByQuestionIds(array($question_id));
1053  $assignmentList = new ilAssQuestionSkillAssignmentList($this->db);
1054  $assignmentList->setParentObjId($obj_id);
1055  $assignmentList->setQuestionIdFilter($question_id);
1056  $assignmentList->loadFromDb();
1057  foreach ($assignmentList->getAssignmentsByQuestionId($question_id) as $assignment) {
1058  /* @var ilAssQuestionSkillAssignment $assignment */
1059  $assignment->deleteFromDb();
1060 
1061  // remove skill usage
1062  if (!$assignment->isSkillUsed()) {
1063  $this->skillUsageService->removeUsage(
1064  $assignment->getParentObjId(),
1065  $assignment->getSkillBaseId(),
1066  $assignment->getSkillTrefId()
1067  );
1068  }
1069  }
1070 
1071  $this->deleteTaxonomyAssignments();
1072  $this->deleteComments();
1073 
1074  try {
1076  } catch (Exception $e) {
1077  $this->ilLog->root()->error("EXCEPTION: Error updating the question pool question count of question pool " . $this->getObjId() . " when deleting question $question_id: $e");
1078  }
1079  }
1080 
1081  private function deleteTaxonomyAssignments(): void
1082  {
1083  $taxIds = ilObjTaxonomy::getUsageOfObject($this->getObjId());
1084 
1085  foreach ($taxIds as $taxId) {
1086  $taxNodeAssignment = new ilTaxNodeAssignment('qpl', $this->getObjId(), 'quest', $taxId);
1087  $taxNodeAssignment->deleteAssignmentsOfItem($this->getId());
1088  }
1089  }
1090 
1091  public function getTotalAnswers(): int
1092  {
1093  // get all question references to the question id
1094  $result = $this->db->queryF(
1095  "SELECT question_id FROM qpl_questions WHERE original_id = %s OR question_id = %s",
1096  array('integer','integer'),
1097  array($this->id, $this->id)
1098  );
1099  if ($this->db->numRows($result) == 0) {
1100  return 0;
1101  }
1102  $found_id = [];
1103  while ($row = $this->db->fetchAssoc($result)) {
1104  $found_id[] = $row["question_id"];
1105  }
1106 
1107  $result = $this->db->query("SELECT * FROM tst_test_result WHERE " . $this->db->in('question_fi', $found_id, false, 'integer'));
1108 
1109  return $this->db->numRows($result);
1110  }
1111 
1112  public static function isFileAvailable(string $file): bool
1113  {
1114  if (!file_exists($file)) {
1115  return false;
1116  }
1117 
1118  if (!is_file($file)) {
1119  return false;
1120  }
1121 
1122  if (!is_readable($file)) {
1123  return false;
1124  }
1125 
1126  return true;
1127  }
1128 
1129  public function copyXHTMLMediaObjectsOfQuestion(int $a_q_id): void
1130  {
1131  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $a_q_id);
1132  foreach ($mobs as $mob) {
1133  ilObjMediaObject::_saveUsage($mob, "qpl:html", $this->getId());
1134  }
1135  }
1136 
1137  public function syncXHTMLMediaObjectsOfQuestion(): void
1138  {
1139  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1140  foreach ($mobs as $mob) {
1141  ilObjMediaObject::_saveUsage($mob, "qpl:html", $this->original_id);
1142  }
1143  }
1144 
1145  public function createPageObject(): void
1146  {
1147  $qpl_id = $this->getObjId();
1148  $this->page = new ilAssQuestionPage(0);
1149  $this->page->setId($this->getId());
1150  $this->page->setParentId($qpl_id);
1151  $this->page->setXMLContent("<PageObject><PageContent>" .
1152  "<Question QRef=\"il__qst_" . $this->getId() . "\"/>" .
1153  "</PageContent></PageObject>");
1154  $this->page->create(false);
1155  }
1156 
1157  public function copyPageOfQuestion(int $a_q_id): void
1158  {
1159  if ($a_q_id > 0) {
1160  $page = new ilAssQuestionPage($a_q_id);
1161  $page->buildDom();
1162  ilPCPlugged::handleCopiedPluggedContent($page, $page->getDomDoc());
1163  $xml = str_replace("il__qst_" . $a_q_id, "il__qst_" . $this->id, $page->getXMLFromDom());
1164  $this->page->setXMLContent($xml);
1165  $this->page->updateFromXML();
1166  }
1167  }
1168 
1169  public function getPageOfQuestion(): string
1170  {
1171  $page = new ilAssQuestionPage($this->id);
1172  return $page->getXMLContent();
1173  }
1174 
1175  public function setOriginalId(?int $original_id): void
1176  {
1177  $this->original_id = $original_id;
1178  }
1179 
1180  public function getOriginalId(): ?int
1181  {
1182  return $this->original_id;
1183  }
1184 
1185  protected static $imageSourceFixReplaceMap = array(
1186  'ok.svg' => 'ok.png',
1187  'not_ok.svg' => 'not_ok.png',
1188  'object/checkbox_checked.svg' => 'checkbox_checked.png',
1189  'object/checkbox_unchecked.svg' => 'checkbox_unchecked.png',
1190  'object/radiobutton_checked.svg' => 'radiobutton_checked.png',
1191  'object/radiobutton_unchecked.svg' => 'radiobutton_unchecked.png'
1192  );
1193 
1194  public function fixSvgToPng(string $imageFilenameContainingString): string
1195  {
1196  $needles = array_keys(self::$imageSourceFixReplaceMap);
1197  $replacements = array_values(self::$imageSourceFixReplaceMap);
1198  return str_replace($needles, $replacements, $imageFilenameContainingString);
1199  }
1200 
1201  public function fixUnavailableSkinImageSources(string $html): string
1202  {
1203  $matches = null;
1204  if (preg_match_all('/src="(.*?)"/m', $html, $matches)) {
1205  $sources = $matches[1];
1206 
1207  $needleReplacementMap = [];
1208 
1209  foreach ($sources as $src) {
1210  $file = ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH) . DIRECTORY_SEPARATOR . $src;
1211 
1212  if (file_exists($file)) {
1213  continue;
1214  }
1215 
1216  $levels = explode(DIRECTORY_SEPARATOR, $src);
1217  if (count($levels) < 5 || $levels[0] !== 'Customizing' || $levels[2] !== 'skin') {
1218  continue;
1219  }
1220 
1221  $component = '';
1222 
1223  if ($levels[4] === 'Modules' || $levels[4] === 'Services') {
1224  $component = $levels[4] . DIRECTORY_SEPARATOR . $levels[5];
1225  }
1226 
1227  $needleReplacementMap[$src] = ilUtil::getImagePath(basename($src), $component);
1228  }
1229 
1230  if (count($needleReplacementMap)) {
1231  $html = str_replace(array_keys($needleReplacementMap), array_values($needleReplacementMap), $html);
1232  }
1233  }
1234 
1235  return $html;
1236  }
1237 
1238  public function loadFromDb(int $question_id): void
1239  {
1240  $result = $this->db->queryF(
1241  'SELECT external_id FROM qpl_questions WHERE question_id = %s',
1242  ['integer'],
1243  [$question_id]
1244  );
1245  if ($this->db->numRows($result) === 1) {
1246  $data = $this->db->fetchAssoc($result);
1247  $this->external_id = $data['external_id'];
1248  }
1249 
1250  $suggested_solutions = $this->loadSuggestedSolutions();
1251  $this->suggested_solutions = array();
1252  if ($suggested_solutions) {
1253  foreach ($suggested_solutions as $solution) {
1254  $this->suggested_solutions[$solution->getSubquestionIndex()] = $solution;
1255  }
1256  }
1257  }
1258 
1265  public function createNewQuestion(bool $a_create_page = true): int
1266  {
1267  $ilUser = $this->current_user;
1268 
1269  $complete = "0";
1270  $obj_id = ($this->getObjId() <= 0) ? (ilObject::_lookupObjId((strlen($this->dic->testQuestionPool()->internal()->request()->getRefId())) ? $this->dic->testQuestionPool()->internal()->request()->getRefId() : $_POST["sel_qpl"])) : $this->getObjId();
1271  if ($obj_id > 0) {
1272  if ($a_create_page) {
1273  $tstamp = 0;
1274  } else {
1275  // question pool must not try to purge
1276  $tstamp = time();
1277  }
1278 
1279  $next_id = $this->db->nextId('qpl_questions');
1280  $this->db->insert("qpl_questions", array(
1281  "question_id" => array("integer", $next_id),
1282  "question_type_fi" => array("integer", $this->getQuestionTypeID()),
1283  "obj_fi" => array("integer", $obj_id),
1284  "title" => array("text", null),
1285  "description" => array("text", null),
1286  "author" => array("text", $this->getAuthor()),
1287  "owner" => array("integer", $ilUser->getId()),
1288  "question_text" => array("clob", null),
1289  "points" => array("float", "0.0"),
1290  "nr_of_tries" => array("integer", $this->getDefaultNrOfTries()), // #10771
1291  "complete" => array("text", $complete),
1292  "created" => array("integer", time()),
1293  "original_id" => array("integer", null),
1294  "tstamp" => array("integer", $tstamp),
1295  "external_id" => array("text", $this->getExternalId()),
1296  'add_cont_edit_mode' => array('text', $this->getAdditionalContentEditingMode())
1297  ));
1298  $this->setId($next_id);
1299 
1300  if ($a_create_page) {
1301  // create page object of question
1302  $this->createPageObject();
1303  }
1304  }
1305 
1306  return $this->getId();
1307  }
1308 
1309  public function saveQuestionDataToDb(int $original_id = -1): void
1310  {
1311  if ($this->getId() == -1) {
1312  $next_id = $this->db->nextId('qpl_questions');
1313  $this->db->insert("qpl_questions", array(
1314  "question_id" => array("integer", $next_id),
1315  "question_type_fi" => array("integer", $this->getQuestionTypeID()),
1316  "obj_fi" => array("integer", $this->getObjId()),
1317  "title" => ["text", mb_substr($this->getTitle(), 0, 124)],
1318  "description" => ["text", mb_substr($this->getComment(), 0, 1000)],
1319  "author" => ["text", mb_substr($this->getAuthor(), 0, 512)],
1320  "owner" => array("integer", $this->getOwner()),
1321  "question_text" => array("clob", ilRTE::_replaceMediaObjectImageSrc($this->getQuestion(), 0)),
1322  "points" => array("float", $this->getMaximumPoints()),
1323  "nr_of_tries" => array("integer", $this->getNrOfTries()),
1324  "created" => array("integer", time()),
1325  "original_id" => array("integer", ($original_id != -1) ? $original_id : null),
1326  "tstamp" => array("integer", time()),
1327  "external_id" => array("text", $this->getExternalId()),
1328  'add_cont_edit_mode' => array('text', $this->getAdditionalContentEditingMode())
1329  ));
1330  $this->setId($next_id);
1331  // create page object of question
1332  $this->createPageObject();
1333  } else {
1334  // Vorhandenen Datensatz aktualisieren
1335  $this->db->update("qpl_questions", array(
1336  "obj_fi" => array("integer", $this->getObjId()),
1337  "title" => ["text", mb_substr($this->getTitle(), 0, 124)],
1338  "description" => ["text", mb_substr($this->getComment(), 0, 1000)],
1339  "author" => ["text", mb_substr($this->getAuthor(), 0, 512)],
1340  "question_text" => array("clob", ilRTE::_replaceMediaObjectImageSrc($this->getQuestion(), 0)),
1341  "points" => array("float", $this->getMaximumPoints()),
1342  "nr_of_tries" => array("integer", $this->getNrOfTries()),
1343  "tstamp" => array("integer", time()),
1344  'complete' => array('integer', $this->isComplete()),
1345  "external_id" => array("text", $this->getExternalId())
1346  ), array(
1347  "question_id" => array("integer", $this->getId())
1348  ));
1349  }
1350  }
1351 
1352  public function saveToDb(): void
1353  {
1354  // remove unused media objects from ILIAS
1355  $this->cleanupMediaObjectUsage();
1356 
1357  $complete = "0";
1358  if ($this->isComplete()) {
1359  $complete = "1";
1360  }
1361 
1362  $this->db->update(
1363  'qpl_questions',
1364  [
1365  'tstamp' => ['integer', time()],
1366  'owner' => ['integer', $this->getOwner()],
1367  'complete' => ['integer', $complete],
1368  'lifecycle' => ['text', $this->getLifecycle()->getIdentifier()],
1369  ],
1370  [
1371  'question_id' => array('integer', $this->getId())
1372  ]
1373  );
1374 
1376  }
1377 
1378  public static function saveOriginalId(int $questionId, int $originalId): void
1379  {
1380  global $DIC;
1381  $ilDB = $DIC->database();
1382  $query = "UPDATE qpl_questions SET tstamp = %s, original_id = %s WHERE question_id = %s";
1383 
1384  $ilDB->manipulateF(
1385  $query,
1386  ['integer','integer', 'text'],
1387  [time(), $originalId, $questionId]
1388  );
1389  }
1390 
1391  public static function resetOriginalId(int $questionId): void
1392  {
1393  global $DIC;
1394  $ilDB = $DIC->database();
1395 
1396  $query = "UPDATE qpl_questions SET tstamp = %s, original_id = NULL WHERE question_id = %s";
1397 
1398  $ilDB->manipulateF(
1399  $query,
1400  ['integer', 'text'],
1401  [time(), $questionId]
1402  );
1403  }
1404 
1405  protected function onDuplicate(int $originalParentId, int $originalQuestionId, int $duplicateParentId, int $duplicateQuestionId): void
1406  {
1407  $this->copySuggestedSolutions($duplicateQuestionId);
1408  $this->duplicateSuggestedSolutionFiles($originalParentId, $originalQuestionId);
1409  $this->feedbackOBJ->duplicateFeedback($originalQuestionId, $duplicateQuestionId);
1410  $this->duplicateQuestionHints($originalQuestionId, $duplicateQuestionId);
1411  $this->duplicateSkillAssignments($originalParentId, $originalQuestionId, $duplicateParentId, $duplicateQuestionId);
1412  $this->duplicateComments($originalParentId, $originalQuestionId, $duplicateParentId, $duplicateQuestionId);
1413  }
1414 
1415  protected function beforeSyncWithOriginal(int $origQuestionId, int $dupQuestionId, int $origParentObjId, int $dupParentObjId): void
1416  {
1417  }
1418 
1419  protected function afterSyncWithOriginal(int $origQuestionId, int $dupQuestionId, int $origParentObjId, int $dupParentObjId): void
1420  {
1421  $this->feedbackOBJ->syncFeedback($origQuestionId, $dupQuestionId);
1422  }
1423 
1424  protected function onCopy(int $sourceParentId, int $sourceQuestionId, int $targetParentId, int $targetQuestionId): void
1425  {
1426  $this->copySuggestedSolutions($targetQuestionId);
1427  $this->copySuggestedSolutionFiles($sourceParentId, $sourceQuestionId);
1428  $this->feedbackOBJ->duplicateFeedback($sourceQuestionId, $targetQuestionId);
1429  $this->duplicateQuestionHints($sourceQuestionId, $targetQuestionId);
1430  $this->duplicateSkillAssignments($sourceParentId, $sourceQuestionId, $targetParentId, $targetQuestionId);
1431  $this->duplicateComments($sourceParentId, $sourceQuestionId, $targetParentId, $targetQuestionId);
1432  }
1433 
1434  protected function duplicateComments(
1435  int $parent_source_id,
1436  int $source_id,
1437  int $parent_target_id,
1438  int $target_id
1439  ): void {
1440  $manager = $this->getNotesManager();
1441  $data_service = $this->getNotesDataService();
1442  $notes = $manager->getNotesForRepositoryObjIds([$parent_source_id], Note::PUBLIC);
1443  $notes = array_filter(
1444  $notes,
1445  fn($n) => $n->getContext()->getSubObjId() === $source_id
1446  );
1447 
1448  foreach ($notes as $note) {
1449  $new_context = $data_service->context(
1450  $parent_target_id,
1451  $target_id,
1452  $note->getContext()->getType()
1453  );
1454  $new_note = $data_service->note(
1455  -1,
1456  $new_context,
1457  $note->getText(),
1458  $note->getAuthor(),
1459  $note->getType(),
1460  $note->getCreationDate(),
1461  $note->getUpdateDate(),
1462  $note->getRecipient()
1463  );
1464  $manager->createNote($new_note, [], true);
1465  }
1466  }
1467 
1468  protected function deleteComments(): void
1469  {
1470  $repo = $this->getNotesRepo();
1471  $manager = $this->getNotesManager();
1472  $source_id = $this->getId();
1473  $notes = $manager->getNotesForRepositoryObjIds([$this->getObjId()], Note::PUBLIC);
1474  $notes = array_filter(
1475  $notes,
1476  fn($n) => $n->getContext()->getSubObjId() === $source_id
1477  );
1478  foreach ($notes as $note) {
1479  $repo->deleteNote($note->getId());
1480  }
1481  }
1482 
1483  protected function getNotesManager(): NotesManager
1484  {
1485  $service = new NotesService($this->dic);
1486  return $service->internal()->domain()->notes();
1487  }
1488 
1489  protected function getNotesDataService(): NotesInternalDataService
1490  {
1491  $service = new NotesService($this->dic);
1492  return $service->internal()->data();
1493  }
1494 
1495  protected function getNotesRepo(): NotesRepo
1496  {
1497  $service = new NotesService($this->dic);
1498  return $service->internal()->repo()->note();
1499  }
1500 
1501  public function deleteSuggestedSolutions(): void
1502  {
1503  $this->getSuggestedSolutionsRepo()->deleteForQuestion($this->getId());
1506  $this->suggested_solutions = [];
1507  }
1508 
1509 
1510  public function getSuggestedSolution(int $subquestion_index = 0): ?assQuestionSuggestedSolution
1511  {
1512  if (array_key_exists($subquestion_index, $this->suggested_solutions)) {
1513  return $this->suggested_solutions[$subquestion_index];
1514  }
1515  return null;
1516  }
1517 
1518  protected function syncSuggestedSolutions(
1519  int $target_question_id,
1520  int $target_obj_id
1521  ): void {
1522  $this->getSuggestedSolutionsRepo()->syncForQuestion($this->getId(), $target_question_id);
1523  $this->syncSuggestedSolutionFiles($target_question_id, $target_obj_id);
1524  }
1525 
1529  protected function duplicateSuggestedSolutionFiles(int $parent_id, int $question_id): void
1530  {
1531  foreach ($this->suggested_solutions as $index => $solution) {
1532  if (!$solution->isOfTypeFile()) {
1533  continue;
1534  }
1535 
1536  $filepath = $this->getSuggestedSolutionPath();
1537  $filepath_original = str_replace(
1538  "/{$this->obj_id}/{$this->id}/solution",
1539  "/$parent_id/$question_id/solution",
1540  $filepath
1541  );
1542  if (!file_exists($filepath)) {
1543  ilFileUtils::makeDirParents($filepath);
1544  }
1545  $filename = $solution->getFilename();
1546  if (strlen($filename) &&
1547  !copy($filepath_original . $filename, $filepath . $filename)) {
1548  $this->ilLog->root()->error("File could not be duplicated!!!!");
1549  $this->ilLog->root()->error("object: " . print_r($this, true));
1550  }
1551  }
1552  }
1553 
1554  protected function syncSuggestedSolutionFiles(
1555  int $target_question_id,
1556  int $target_obj_id
1557  ): void {
1558  $filepath = $this->getSuggestedSolutionPath();
1559  $filepath_original = str_replace(
1560  "{$this->getObjId()}/{$this->id}/solution",
1561  "{$target_obj_id}/{$target_question_id}/solution",
1562  $filepath
1563  );
1564  ilFileUtils::delDir($filepath_original);
1565  foreach ($this->suggested_solutions as $index => $solution) {
1566  if ($solution->isOfTypeFile()) {
1567  if (!file_exists($filepath_original)) {
1568  ilFileUtils::makeDirParents($filepath_original);
1569  }
1570  $filename = $solution->getFilename();
1571  if (strlen($filename)) {
1572  if (!@copy($filepath . $filename, $filepath_original . $filename)) {
1573  $this->ilLog->root()->error("File could not be duplicated!!!!");
1574  $this->ilLog->root()->error("object: " . print_r($this, true));
1575  }
1576  }
1577  }
1578  }
1579  }
1580 
1581  protected function copySuggestedSolutionFiles(int $source_questionpool_id, int $source_question_id): void
1582  {
1583  foreach ($this->suggested_solutions as $index => $solution) {
1584  if ($solution->isOfTypeFile()) {
1585  $filepath = $this->getSuggestedSolutionPath();
1586  $filepath_original = str_replace("/$this->obj_id/$this->id/solution", "/$source_questionpool_id/$source_question_id/solution", $filepath);
1587  if (!file_exists($filepath)) {
1588  ilFileUtils::makeDirParents($filepath);
1589  }
1590  $filename = $solution->getFilename();
1591  if ($filename !== '') {
1592  if (!copy($filepath_original . $filename, $filepath . $filename)) {
1593  $this->ilLog->root()->error("File could not be copied!!!!");
1594  $this->ilLog->root()->error("object: " . print_r($this, true));
1595  }
1596  }
1597  }
1598  }
1599  }
1600 
1601  protected function copySuggestedSolutions(int $target_question_id): void
1602  {
1603  $update = [];
1604  foreach ($this->getSuggestedSolutions() as $index => $solution) {
1605  $solution = $solution->withQuestionId($target_question_id);
1606  $update[] = $solution;
1607  }
1608  $this->getSuggestedSolutionsRepo()->update($update);
1609  }
1610 
1611  public function resolveInternalLink(string $internal_link): string
1612  {
1613  if (preg_match("/il_(\d+)_(\w+)_(\d+)/", $internal_link, $matches)) {
1614  switch ($matches[2]) {
1615  case "lm":
1616  $resolved_link = ilLMObject::_getIdForImportId($internal_link);
1617  break;
1618  case "pg":
1619  $resolved_link = ilInternalLink::_getIdForImportId("PageObject", $internal_link);
1620  break;
1621  case "st":
1622  $resolved_link = ilInternalLink::_getIdForImportId("StructureObject", $internal_link);
1623  break;
1624  case "git":
1625  $resolved_link = ilInternalLink::_getIdForImportId("GlossaryItem", $internal_link);
1626  break;
1627  case "mob":
1628  $resolved_link = ilInternalLink::_getIdForImportId("MediaObject", $internal_link);
1629  break;
1630  }
1631  if ($resolved_link === null | $resolved_link === 0) {
1632  $resolved_link = "il__{$matches[2]}_{$matches[3]}";
1633  }
1634  } else {
1635  $resolved_link = $internal_link;
1636  }
1637  return $resolved_link ?? '';
1638  }
1639 
1640  public function resolveSuggestedSolutionLinks(): void
1641  {
1642  $resolvedlinks = 0;
1643  $result = $this->db->queryF(
1644  "SELECT * FROM qpl_sol_sug WHERE question_fi = %s",
1645  array('integer'),
1646  array($this->getId())
1647  );
1648  if ($this->db->numRows($result) > 0) {
1649  while ($row = $this->db->fetchAssoc($result)) {
1650  $internal_link = $row["internal_link"];
1651  $resolved_link = $this->resolveInternalLink($internal_link);
1652  if ($internal_link !== $resolved_link) {
1653  // internal link was resolved successfully
1654  $this->db->manipulateF(
1655  "UPDATE qpl_sol_sug SET internal_link = %s WHERE suggested_solution_id = %s",
1656  array('text','integer'),
1657  array($resolved_link, $row["suggested_solution_id"])
1658  );
1659  $resolvedlinks++;
1660  }
1661  }
1662  }
1663  if ($resolvedlinks) {
1664  // there are resolved links -> reenter theses links to the database
1665  // delete all internal links from the database
1667 
1668  $result = $this->db->queryF(
1669  "SELECT * FROM qpl_sol_sug WHERE question_fi = %s",
1670  array('integer'),
1671  array($this->getId())
1672  );
1673  if ($this->db->numRows($result) > 0) {
1674  while ($row = $this->db->fetchAssoc($result)) {
1675  if (preg_match("/il_(\d*?)_(\w+)_(\d+)/", $row["internal_link"], $matches)) {
1676  ilInternalLink::_saveLink("qst", $this->getId(), $matches[2], $matches[3], $matches[1]);
1677  }
1678  }
1679  }
1680  }
1681  }
1682 
1683  public static function _getInternalLinkHref(string $target = ""): string
1684  {
1685  global $DIC;
1686  $linktypes = array(
1687  "lm" => "LearningModule",
1688  "pg" => "PageObject",
1689  "st" => "StructureObject",
1690  "git" => "GlossaryItem",
1691  "mob" => "MediaObject"
1692  );
1693  $href = "";
1694  if (preg_match("/il__(\w+)_(\d+)/", $target, $matches)) {
1695  $type = $matches[1];
1696  $target_id = $matches[2];
1697  switch ($linktypes[$matches[1]]) {
1698  case "MediaObject":
1699  $href = "./ilias.php?baseClass=ilLMPresentationGUI&obj_type=" . $linktypes[$type]
1700  . "&cmd=media&ref_id=" . $DIC->testQuestionPool()->internal()->request()->getRefId()
1701  . "&mob_id=" . $target_id;
1702  break;
1703  case "StructureObject":
1704  case "GlossaryItem":
1705  case "PageObject":
1706  case "LearningModule":
1707  default:
1708  $href = "./goto.php?target=" . $type . "_" . $target_id;
1709  break;
1710  }
1711  }
1712  return $href;
1713  }
1714 
1715  public function syncWithOriginal(): void
1716  {
1717  $original_id = $this->getOriginalId();
1718  if ($original_id === null) {
1719  return; // No original -> no sync
1720  }
1721 
1722  $original_obj_id = self::lookupParentObjId($this->getOriginalId());
1723 
1724  if (!$original_obj_id) {
1725  return; // Original does not exist -> no sync
1726  }
1727 
1728  $this->beforeSyncWithOriginal($this->getOriginalId(), $this->getId(), $original_obj_id, $this->getObjId());
1729 
1730  $original = clone $this;
1731  // Now we become the original
1732  $original->setId($this->getOriginalId());
1733  $original->setOriginalId(null);
1734  $original->setObjId($original_obj_id);
1735 
1736  $original->saveToDb();
1737 
1738  $original->deletePageOfQuestion($this->getOriginalId());
1739  $original->createPageObject();
1740  $original->copyPageOfQuestion($this->getId());
1741 
1742  $this->syncSuggestedSolutions($this->getOriginalId(), $original_obj_id);
1744  $this->afterSyncWithOriginal($this->getOriginalId(), $this->getId(), $original_obj_id, $this->getObjId());
1745  $this->syncHints();
1746  }
1747 
1753  public static function instantiateQuestion(int $question_id): assQuestion
1754  {
1755  global $DIC;
1756  $ilCtrl = $DIC['ilCtrl'];
1757  $ilDB = $DIC['ilDB'];
1758  $lng = $DIC['lng'];
1759  $questioninfo = $DIC->testQuestionPool()->questionInfo();
1760  $question_type = $questioninfo->getQuestionType($question_id);
1761  if ($question_type === '') {
1762  throw new InvalidArgumentException('No question with ID ' . $question_id . ' exists');
1763  }
1764 
1765  $question = new $question_type();
1766  $question->loadFromDb($question_id);
1767 
1768  $feedbackObjectClassname = self::getFeedbackClassNameByQuestionType($question_type);
1769  $question->feedbackOBJ = new $feedbackObjectClassname($question, $ilCtrl, $ilDB, $lng);
1770 
1771  return $question;
1772  }
1773 
1774  public function getPoints(): float
1775  {
1776  if (strcmp($this->points, "") == 0) {
1777  return 0.0;
1778  }
1779 
1780  return $this->points;
1781  }
1782 
1783  public function setPoints(float $points): void
1784  {
1785  $this->points = $points;
1786  }
1787 
1788  public function getSolutionMaxPass(int $active_id): ?int
1789  {
1790  return self::_getSolutionMaxPass($this->getId(), $active_id);
1791  }
1792 
1796  public static function _getSolutionMaxPass(int $question_id, int $active_id): ?int
1797  {
1798  // the following code was the old solution which added the non answered
1799  // questions of a pass from the answered questions of the previous pass
1800  // with the above solution, only the answered questions of the last pass are counted
1801  global $DIC;
1802  $ilDB = $DIC['ilDB'];
1803 
1804  $result = $ilDB->queryF(
1805  "SELECT MAX(pass) maxpass FROM tst_test_result WHERE active_fi = %s AND question_fi = %s",
1806  array('integer','integer'),
1807  array($active_id, $question_id)
1808  );
1809  if ($result->numRows() === 1) {
1810  $row = $ilDB->fetchAssoc($result);
1811  return $row["maxpass"];
1812  }
1813 
1814  return null;
1815  }
1816 
1817  public static function _isWriteable(int $question_id, int $user_id): bool
1818  {
1819  global $DIC;
1820  $ilDB = $DIC['ilDB'];
1821 
1822  if (($question_id < 1) || ($user_id < 1)) {
1823  return false;
1824  }
1825 
1826  $result = $ilDB->queryF(
1827  "SELECT obj_fi FROM qpl_questions WHERE question_id = %s",
1828  array('integer'),
1829  array($question_id)
1830  );
1831  if ($ilDB->numRows($result) == 1) {
1832  $row = $ilDB->fetchAssoc($result);
1833  $qpl_object_id = (int) $row["obj_fi"];
1834  return ilObjQuestionPool::_isWriteable($qpl_object_id, $user_id);
1835  }
1836 
1837  return false;
1838  }
1839 
1851  abstract public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false): float|array;
1852 
1853  public function deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $previewSession, $reachedPoints): ?float
1854  {
1855  global $DIC;
1856 
1857  $hintTracking = new ilAssQuestionPreviewHintTracking($DIC->database(), $previewSession);
1858  $requestsStatisticData = $hintTracking->getRequestStatisticData();
1859  $reachedPoints = $reachedPoints - $requestsStatisticData->getRequestsPoints();
1860 
1861  return $reachedPoints;
1862  }
1863 
1865  {
1866  $reachedPoints = $this->calculateReachedPointsForSolution($previewSession->getParticipantsSolution());
1867  $reachedPoints = $this->deductHintPointsFromReachedPoints($previewSession, $reachedPoints);
1868 
1869  return $this->ensureNonNegativePoints($reachedPoints);
1870  }
1871 
1872  protected function ensureNonNegativePoints($points)
1873  {
1874  return $points > 0 ? $points : 0;
1875  }
1876 
1877  public function isPreviewSolutionCorrect(ilAssQuestionPreviewSession $previewSession): bool
1878  {
1879  $reachedPoints = $this->calculateReachedPointsFromPreviewSession($previewSession);
1880 
1881  return !($reachedPoints < $this->getMaximumPoints());
1882  }
1883 
1884 
1895  final public function adjustReachedPointsByScoringOptions($points, $active_id, $pass = null): float
1896  {
1897  $count_system = ilObjTest::_getCountSystem($active_id);
1898  if ($count_system == 1) {
1899  if (abs($this->getMaximumPoints() - $points) > 0.0000000001) {
1900  $points = 0;
1901  }
1902  }
1903  $score_cutting = ilObjTest::_getScoreCutting($active_id);
1904  if ($score_cutting == 0) {
1905  if ($points < 0) {
1906  $points = 0;
1907  }
1908  }
1909  return $points;
1910  }
1911  public function buildHashedImageFilename(string $plain_image_filename, bool $unique = false): string
1912  {
1913  $extension = "";
1914 
1915  if (preg_match("/.*\.(png|jpg|gif|jpeg)$/i", $plain_image_filename, $matches)) {
1916  $extension = "." . $matches[1];
1917  }
1918 
1919  if ($unique) {
1920  $plain_image_filename = uniqid($plain_image_filename . microtime(true), true);
1921  }
1922 
1923  return md5($plain_image_filename) . $extension;
1924  }
1925 
1936  public static function _setReachedPoints(
1937  int $active_id,
1938  int $question_id,
1939  float $points,
1940  float $maxpoints,
1941  int $pass,
1942  bool $manualscoring,
1943  bool $obligationsEnabled,
1944  ?int $test_id = null
1945  ): bool {
1946  global $DIC;
1947  $ilDB = $DIC['ilDB'];
1948  $refinery = $DIC['refinery'];
1949 
1950  $float_trafo = $refinery->kindlyTo()->float();
1951  try {
1952  $points = $float_trafo->transform($points);
1953  } catch (ILIAS\Refinery\ConstraintViolationException $e) {
1954  return false;
1955  }
1956 
1957  if ($points <= $maxpoints) {
1958  if ($pass === null) {
1959  $pass = assQuestion::_getSolutionMaxPass($question_id, $active_id);
1960  }
1961 
1962  $rowsnum = 0;
1963  $old_points = 0;
1964 
1965  if ($pass !== null) {
1966  $result = $ilDB->queryF(
1967  "SELECT points FROM tst_test_result WHERE active_fi = %s AND question_fi = %s AND pass = %s",
1968  array('integer','integer','integer'),
1969  array($active_id, $question_id, $pass)
1970  );
1971  $manual = ($manualscoring) ? 1 : 0;
1972  $rowsnum = $result->numRows();
1973  }
1974  if ($rowsnum > 0) {
1975  $row = $ilDB->fetchAssoc($result);
1976  $old_points = $row["points"];
1977  if ($old_points != $points) {
1978  $affectedRows = $ilDB->manipulateF(
1979  "UPDATE tst_test_result SET points = %s, manual = %s, tstamp = %s WHERE active_fi = %s AND question_fi = %s AND pass = %s",
1980  array('float', 'integer', 'integer', 'integer', 'integer', 'integer'),
1981  array($points, $manual, time(), $active_id, $question_id, $pass)
1982  );
1983  }
1984  } else {
1985  $next_id = $ilDB->nextId('tst_test_result');
1986  $affectedRows = $ilDB->manipulateF(
1987  "INSERT INTO tst_test_result (test_result_id, active_fi, question_fi, points, pass, manual, tstamp) VALUES (%s, %s, %s, %s, %s, %s, %s)",
1988  array('integer', 'integer','integer', 'float', 'integer', 'integer','integer'),
1989  array($next_id, $active_id, $question_id, $points, $pass, $manual, time())
1990  );
1991  }
1992 
1993  if (self::isForcePassResultUpdateEnabled() || $old_points != $points || $rowsnum == 0) {
1994  $test_id = $test_id ? (int) ilObjTest::_getObjectIDFromTestID($test_id) : ilObjTest::_lookupTestObjIdForQuestionId($question_id);
1995  if ($test_id === null || $test_id == 0) {
1996  return false;
1997  }
1998  $test = new ilObjTest(
1999  $test_id,
2000  false
2001  );
2002  $test->updateTestPassResults($active_id, $pass, $obligationsEnabled);
2005  global $DIC;
2006  $lng = $DIC['lng'];
2007  $ilUser = $DIC['ilUser'];
2008  $username = ilObjTestAccess::_getParticipantData($active_id);
2009  assQuestion::logAction(sprintf(
2010  $lng->txtlng(
2011  "assessment",
2012  "log_answer_changed_points",
2014  ),
2015  $username,
2016  $old_points,
2017  $points,
2018  $ilUser->getFullname() . " (" . $ilUser->getLogin() . ")"
2019  ), $active_id, $question_id);
2020  }
2021  }
2022 
2023  return true;
2024  }
2025 
2026  return false;
2027  }
2028 
2029  public function getQuestion(): string
2030  {
2031  return $this->question;
2032  }
2033 
2034  public function getQuestionForHTMLOutput(): string
2035  {
2036  return $this->purifyAndPrepareTextAreaOutput($this->question);
2037  }
2038 
2039  protected function purifyAndPrepareTextAreaOutput(string $content): string
2040  {
2041  $purified_content = $this->getHtmlQuestionContentPurifier()->purify($content);
2043  || !(new ilSetting('advanced_editing'))->get('advanced_editing_javascript_editor') === 'tinymce') {
2044  $purified_content = nl2br($purified_content);
2045  }
2047  $purified_content,
2048  true,
2049  true
2050  );
2051  }
2052 
2053  public function setQuestion(string $question = ""): void
2054  {
2055  $this->question = $question;
2056  }
2057 
2063  abstract public function getQuestionType(): string;
2064 
2065  public function getQuestionTypeID(): int
2066  {
2067  $result = $this->db->queryF(
2068  "SELECT question_type_id FROM qpl_qst_type WHERE type_tag = %s",
2069  array('text'),
2070  array($this->getQuestionType())
2071  );
2072  if ($this->db->numRows($result) == 1) {
2073  $row = $this->db->fetchAssoc($result);
2074  return (int) $row["question_type_id"];
2075  }
2076  return 0;
2077  }
2078 
2079  public function syncHints(): void
2080  {
2081  // delete hints of the original
2082  $this->db->manipulateF(
2083  "DELETE FROM qpl_hints WHERE qht_question_fi = %s",
2084  array('integer'),
2085  array($this->original_id)
2086  );
2087 
2088  // get hints of the actual question
2089  $result = $this->db->queryF(
2090  "SELECT * FROM qpl_hints WHERE qht_question_fi = %s",
2091  array('integer'),
2092  array($this->getId())
2093  );
2094 
2095  // save hints to the original
2096  if ($this->db->numRows($result) > 0) {
2097  while ($row = $this->db->fetchAssoc($result)) {
2098  $next_id = $this->db->nextId('qpl_hints');
2099  $this->db->insert(
2100  'qpl_hints',
2101  array(
2102  'qht_hint_id' => array('integer', $next_id),
2103  'qht_question_fi' => array('integer', $this->original_id),
2104  'qht_hint_index' => array('integer', $row["qht_hint_index"]),
2105  'qht_hint_points' => array('float', $row["qht_hint_points"]),
2106  'qht_hint_text' => array('text', $row["qht_hint_text"]),
2107  )
2108  );
2109  }
2110  }
2111  }
2112 
2113  protected function getRTETextWithMediaObjects(): string
2114  {
2115  // must be called in parent classes. add additional RTE text in the parent
2116  // classes and call this method to add the standard RTE text
2117  $collected = $this->getQuestion();
2118  $collected .= $this->feedbackOBJ->getGenericFeedbackContent($this->getId(), false);
2119  $collected .= $this->feedbackOBJ->getGenericFeedbackContent($this->getId(), true);
2120  $collected .= $this->feedbackOBJ->getAllSpecificAnswerFeedbackContents($this->getId());
2121 
2122  $questionHintList = ilAssQuestionHintList::getListByQuestionId($this->getId());
2123  foreach ($questionHintList as $questionHint) {
2124  /* @var $questionHint ilAssQuestionHint */
2125  $collected .= $questionHint->getText();
2126  }
2127 
2128  return $collected;
2129  }
2130 
2131  public function cleanupMediaObjectUsage(): void
2132  {
2133  $combinedtext = $this->getRTETextWithMediaObjects();
2134  ilRTE::_cleanupMediaObjectUsage($combinedtext, "qpl:html", $this->getId());
2135  }
2136 
2137  public function getInstances(): array
2138  {
2139  $result = $this->db->queryF(
2140  "SELECT question_id FROM qpl_questions WHERE original_id = %s",
2141  array("integer"),
2142  array($this->getId())
2143  );
2144  $instances = [];
2145  $ids = [];
2146  while ($row = $this->db->fetchAssoc($result)) {
2147  $ids[] = $row["question_id"];
2148  }
2149  foreach ($ids as $question_id) {
2150  // check non random tests
2151  $result = $this->db->queryF(
2152  "SELECT tst_tests.obj_fi FROM tst_tests, tst_test_question WHERE tst_test_question.question_fi = %s AND tst_test_question.test_fi = tst_tests.test_id",
2153  array("integer"),
2154  array($question_id)
2155  );
2156  while ($row = $this->db->fetchAssoc($result)) {
2157  $instances[$row['obj_fi']] = ilObject::_lookupTitle($row['obj_fi']);
2158  }
2159  // check random tests
2160  $result = $this->db->queryF(
2161  "SELECT tst_tests.obj_fi FROM tst_tests, tst_test_rnd_qst, tst_active WHERE tst_test_rnd_qst.active_fi = tst_active.active_id AND tst_test_rnd_qst.question_fi = %s AND tst_tests.test_id = tst_active.test_fi",
2162  array("integer"),
2163  array($question_id)
2164  );
2165  while ($row = $this->db->fetchAssoc($result)) {
2166  $instances[$row['obj_fi']] = ilObject::_lookupTitle($row['obj_fi']);
2167  }
2168  }
2169  foreach ($instances as $key => $value) {
2170  $instances[$key] = array("obj_id" => $key, "title" => $value, "author" => ilObjTest::_lookupAuthor($key), "refs" => ilObject::_getAllReferences($key));
2171  }
2172  return $instances;
2173  }
2174 
2175  public static function _needsManualScoring(int $question_id): bool
2176  {
2177  global $DIC;
2178  $questioninfo = $DIC->testQuestionPool()->questionInfo();
2180  $questiontype = $questioninfo->getQuestionType($question_id);
2181  if (in_array($questiontype, $scoring)) {
2182  return true;
2183  }
2184 
2185  return false;
2186  }
2187 
2194  public function getActiveUserData(int $active_id): array
2195  {
2196  $result = $this->db->queryF(
2197  "SELECT * FROM tst_active WHERE active_id = %s",
2198  array('integer'),
2199  array($active_id)
2200  );
2201  if ($this->db->numRows($result)) {
2202  $row = $this->db->fetchAssoc($result);
2203  return array("user_id" => $row["user_fi"], "test_id" => $row["test_fi"]);
2204  }
2205 
2206  return [];
2207  }
2208 
2209  public function hasSpecificFeedback(): bool
2210  {
2211  return static::HAS_SPECIFIC_FEEDBACK;
2212  }
2213 
2214  public static function getFeedbackClassNameByQuestionType(string $questionType): string
2215  {
2216  return str_replace('ass', 'ilAss', $questionType) . 'Feedback';
2217  }
2218 
2219 
2220 
2221  public static function instantiateQuestionGUI(int $a_question_id): assQuestionGUI
2222  {
2223  //Shouldn't you live in assQuestionGUI, Mister?
2224 
2225  global $DIC;
2226  $ilCtrl = $DIC['ilCtrl'];
2227  $ilDB = $DIC['ilDB'];
2228  $lng = $DIC['lng'];
2229  $ilUser = $DIC['ilUser'];
2230  $questioninfo = $DIC->testQuestionPool()->questionInfo();
2231  if (strcmp($a_question_id, "") != 0) {
2232  $question_type = $questioninfo->getQuestionType($a_question_id);
2233 
2234  $question_type_gui = $question_type . 'GUI';
2235  $question_gui = new $question_type_gui();
2236  $question_gui->object->loadFromDb($a_question_id);
2237 
2238  $feedbackObjectClassname = self::getFeedbackClassNameByQuestionType($question_type);
2239  $question_gui->object->feedbackOBJ = new $feedbackObjectClassname($question_gui->object, $ilCtrl, $ilDB, $lng);
2240 
2241  $assSettings = new ilSetting('assessment');
2242  $processLockerFactory = new ilAssQuestionProcessLockerFactory($assSettings, $ilDB);
2243  $processLockerFactory->setQuestionId($question_gui->object->getId());
2244  $processLockerFactory->setUserId($ilUser->getId());
2245  $processLockerFactory->setAssessmentLogEnabled(ilObjAssessmentFolder::_enabledAssessmentLogging());
2246  $question_gui->object->setProcessLocker($processLockerFactory->getLocker());
2247  } else {
2248  global $DIC;
2249  $ilLog = $DIC['ilLog'];
2250  $ilLog->write('Instantiate question called without question id. (instantiateQuestionGUI@assQuestion)', $ilLog->WARNING);
2251  throw new InvalidArgumentException('Instantiate question called without question id. (instantiateQuestionGUI@assQuestion)');
2252  }
2253  return $question_gui;
2254  }
2255 
2256  public function setExportDetailsXLSX(ilAssExcelFormatHelper $worksheet, int $startrow, int $col, int $active_id, int $pass): int
2257  {
2258  $worksheet->setFormattedExcelTitle($worksheet->getColumnCoord($col) . $startrow, $this->lng->txt($this->getQuestionType()));
2259  $worksheet->setFormattedExcelTitle($worksheet->getColumnCoord($col + 1) . $startrow, $this->getTitle());
2260 
2261  return $startrow;
2262  }
2263 
2264  public function getNrOfTries(): int
2265  {
2266  return $this->nr_of_tries;
2267  }
2268 
2269  public function setNrOfTries(int $a_nr_of_tries): void
2270  {
2271  $this->nr_of_tries = $a_nr_of_tries;
2272  }
2273 
2274  public function setExportImagePath(string $path): void
2275  {
2276  $this->export_image_path = $path;
2277  }
2278 
2279  public static function _questionExistsInTest(int $question_id, int $test_id): bool
2280  {
2281  global $DIC;
2282  $ilDB = $DIC['ilDB'];
2283 
2284  if ($question_id < 1) {
2285  return false;
2286  }
2287 
2288  $result = $ilDB->queryF(
2289  "SELECT question_fi FROM tst_test_question WHERE question_fi = %s AND test_fi = %s",
2290  array('integer', 'integer'),
2291  array($question_id, $test_id)
2292  );
2293  return $ilDB->numRows($result) == 1;
2294  }
2295 
2296  public function formatSAQuestion($a_q): string
2297  {
2298  return $this->getSelfAssessmentFormatter()->format($a_q);
2299  }
2300 
2302  {
2303  return new \ilAssSelfAssessmentQuestionFormatter();
2304  }
2305 
2306  // scorm2004-start ???
2307 
2308  public function setPreventRteUsage(bool $prevent_rte_usage): void
2309  {
2310  $this->prevent_rte_usage = $prevent_rte_usage;
2311  }
2312 
2313  public function getPreventRteUsage(): bool
2314  {
2315  return $this->prevent_rte_usage;
2316  }
2317 
2319  {
2320  $this->lmMigrateQuestionTypeGenericContent($migrator);
2321  $this->lmMigrateQuestionTypeSpecificContent($migrator);
2322  $this->saveToDb();
2323 
2324  $this->feedbackOBJ->migrateContentForLearningModule($migrator, $this->getId());
2325  }
2326 
2328  {
2329  $this->setQuestion($migrator->migrateToLmContent($this->getQuestion()));
2330  }
2331 
2333  {
2334  // overwrite if any question type specific content except feedback needs to be migrated
2335  }
2336 
2337  public function setSelfAssessmentEditingMode(bool $selfassessmenteditingmode): void
2338  {
2339  $this->selfassessmenteditingmode = $selfassessmenteditingmode;
2340  }
2341 
2342  public function getSelfAssessmentEditingMode(): bool
2343  {
2345  }
2346 
2347  public function setDefaultNrOfTries(int $defaultnroftries): void
2348  {
2349  $this->defaultnroftries = $defaultnroftries;
2350  }
2351 
2352  public function getDefaultNrOfTries(): int
2353  {
2354  return $this->defaultnroftries;
2355  }
2356 
2357  // scorm2004-end ???
2358 
2359  public static function lookupParentObjId(int $questionId): int
2360  {
2361  global $DIC;
2362  $ilDB = $DIC['ilDB'];
2363 
2364  $query = "SELECT obj_fi FROM qpl_questions WHERE question_id = %s";
2365 
2366  $res = $ilDB->queryF($query, array('integer'), array($questionId));
2367  $row = $ilDB->fetchAssoc($res);
2368 
2369  return $row['obj_fi'];
2370  }
2371 
2372  protected function duplicateQuestionHints(int $originalQuestionId, int $duplicateQuestionId): void
2373  {
2374  $hintIds = ilAssQuestionHintList::duplicateListForQuestion($originalQuestionId, $duplicateQuestionId);
2375 
2377  foreach ($hintIds as $originalHintId => $duplicateHintId) {
2378  $this->ensureHintPageObjectExists($originalHintId);
2379  $originalPageObject = new ilAssHintPage($originalHintId);
2380  $originalXML = $originalPageObject->getXMLContent();
2381 
2382  $duplicatePageObject = new ilAssHintPage();
2383  $duplicatePageObject->setId($duplicateHintId);
2384  $duplicatePageObject->setParentId($this->getId());
2385  $duplicatePageObject->setXMLContent($originalXML);
2386  $duplicatePageObject->createFromXML();
2387  }
2388  }
2389  }
2390 
2391  protected function duplicateSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId): void
2392  {
2393  $assignmentList = new ilAssQuestionSkillAssignmentList($this->db);
2394  $assignmentList->setParentObjId($srcParentId);
2395  $assignmentList->setQuestionIdFilter($srcQuestionId);
2396  $assignmentList->loadFromDb();
2397 
2398  foreach ($assignmentList->getAssignmentsByQuestionId($srcQuestionId) as $assignment) {
2399  $assignment->setParentObjId($trgParentId);
2400  $assignment->setQuestionId($trgQuestionId);
2401  $assignment->saveToDb();
2402 
2403  // add skill usage
2404  $this->skillUsageService->addUsage(
2405  $trgParentId,
2406  $assignment->getSkillBaseId(),
2407  $assignment->getSkillTrefId()
2408  );
2409  }
2410  }
2411 
2412  public function syncSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId): void
2413  {
2414  $assignmentList = new ilAssQuestionSkillAssignmentList($this->db);
2415  $assignmentList->setParentObjId($trgParentId);
2416  $assignmentList->setQuestionIdFilter($trgQuestionId);
2417  $assignmentList->loadFromDb();
2418 
2419  foreach ($assignmentList->getAssignmentsByQuestionId($trgQuestionId) as $assignment) {
2420  $assignment->deleteFromDb();
2421 
2422  // remove skill usage
2423  if (!$assignment->isSkillUsed()) {
2424  $this->skillUsageService->removeUsage(
2425  $assignment->getParentObjId(),
2426  $assignment->getSkillBaseId(),
2427  $assignment->getSkillTrefId()
2428  );
2429  }
2430  }
2431 
2432  $this->duplicateSkillAssignments($srcParentId, $srcQuestionId, $trgParentId, $trgQuestionId);
2433  }
2434 
2435  public function ensureHintPageObjectExists($pageObjectId): void
2436  {
2437  if (!ilAssHintPage::_exists('qht', $pageObjectId)) {
2438  $pageObject = new ilAssHintPage();
2439  $pageObject->setParentId($this->getId());
2440  $pageObject->setId($pageObjectId);
2441  $pageObject->createFromXML();
2442  }
2443  }
2444 
2445  public function isAnswered(int $active_id, int $pass): bool
2446  {
2447  $numExistingSolutionRecords = assQuestion::getNumExistingSolutionRecords($active_id, $pass, $this->getId());
2448  return $numExistingSolutionRecords > 0;
2449  }
2450 
2451  public static function isObligationPossible(int $questionId): bool
2452  {
2453  return false;
2454  }
2455 
2456  protected static function getNumExistingSolutionRecords(int $activeId, int $pass, int $questionId): int
2457  {
2458  global $DIC;
2459  $ilDB = $DIC['ilDB'];
2460 
2461  $query = "
2462  SELECT count(active_fi) cnt
2463 
2464  FROM tst_solutions
2465 
2466  WHERE active_fi = %s
2467  AND question_fi = %s
2468  AND pass = %s
2469  ";
2470 
2471  $res = $ilDB->queryF(
2472  $query,
2473  array('integer','integer','integer'),
2474  array($activeId, $questionId, $pass)
2475  );
2476 
2477  $row = $ilDB->fetchAssoc($res);
2478 
2479  return (int) $row['cnt'];
2480  }
2481 
2482  public function getAdditionalContentEditingMode(): string
2483  {
2485  }
2486 
2487  public function setAdditionalContentEditingMode(?string $additionalContentEditingMode): void
2488  {
2489  if (!in_array((string) $additionalContentEditingMode, $this->getValidAdditionalContentEditingModes())) {
2490  throw new ilTestQuestionPoolException('invalid additional content editing mode given: ' . $additionalContentEditingMode);
2491  }
2492 
2493  $this->additionalContentEditingMode = $additionalContentEditingMode;
2494  }
2495 
2497  {
2499  }
2500 
2501  public function isValidAdditionalContentEditingMode(string $additionalContentEditingMode): bool
2502  {
2503  if (in_array($additionalContentEditingMode, $this->getValidAdditionalContentEditingModes())) {
2504  return true;
2505  }
2506 
2507  return false;
2508  }
2509 
2510  public function getValidAdditionalContentEditingModes(): array
2511  {
2512  return array(
2513  self::ADDITIONAL_CONTENT_EDITING_MODE_RTE,
2514  self::ADDITIONAL_CONTENT_EDITING_MODE_IPE
2515  );
2516  }
2517 
2522  {
2523  return ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution');
2524  }
2525 
2530  {
2531  return ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution');
2532  }
2533 
2534  protected function buildQuestionDataQuery(): string
2535  {
2536  return "
2537  SELECT qpl_questions.*,
2538  {$this->getAdditionalTableName()}.*
2539  FROM qpl_questions
2540  LEFT JOIN {$this->getAdditionalTableName()}
2541  ON {$this->getAdditionalTableName()}.question_fi = qpl_questions.question_id
2542  WHERE qpl_questions.question_id = %s
2543  ";
2544  }
2545 
2546  public function setLastChange($lastChange): void
2547  {
2548  $this->lastChange = $lastChange;
2549  }
2550 
2551  public function getLastChange()
2552  {
2553  return $this->lastChange;
2554  }
2555 
2556  protected function getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized = true): \ilDBStatement
2557  {
2558  if ($this->getStep() !== null) {
2559  $query = "
2560  SELECT *
2561  FROM tst_solutions
2562  WHERE active_fi = %s
2563  AND question_fi = %s
2564  AND pass = %s
2565  AND step = %s
2566  AND authorized = %s
2567  ";
2568 
2569  return $this->db->queryF(
2570  $query,
2571  array('integer', 'integer', 'integer', 'integer', 'integer'),
2572  array($active_id, $this->getId(), $pass, $this->getStep(), (int) $authorized)
2573  );
2574  }
2575 
2576  $query = "
2577  SELECT *
2578  FROM tst_solutions
2579  WHERE active_fi = %s
2580  AND question_fi = %s
2581  AND pass = %s
2582  AND authorized = %s
2583  ";
2584 
2585  return $this->db->queryF(
2586  $query,
2587  array('integer', 'integer', 'integer', 'integer'),
2588  array($active_id, $this->getId(), $pass, (int) $authorized)
2589  );
2590  }
2591 
2592  protected function removeSolutionRecordById(int $solutionId): int
2593  {
2594  return $this->db->manipulateF(
2595  "DELETE FROM tst_solutions WHERE solution_id = %s",
2596  array('integer'),
2597  array($solutionId)
2598  );
2599  }
2600 
2601  // hey: prevPassSolutions - selected file reuse, copy solution records
2605  protected function getSolutionRecordById(int $solutionId): array
2606  {
2607  $result = $this->db->queryF(
2608  "SELECT * FROM tst_solutions WHERE solution_id = %s",
2609  array('integer'),
2610  array($solutionId)
2611  );
2612 
2613  if ($this->db->numRows($result) > 0) {
2614  return $this->db->fetchAssoc($result);
2615  }
2616  return [];
2617  }
2618  // hey.
2619 
2620  public function removeIntermediateSolution(int $active_id, int $pass): void
2621  {
2622  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function () use ($active_id, $pass) {
2623  $this->removeCurrentSolution($active_id, $pass, false);
2624  });
2625  }
2626 
2630  public function removeCurrentSolution(int $active_id, int $pass, bool $authorized = true): int
2631  {
2632  if ($this->getStep() !== null) {
2633  $query = '
2634  DELETE FROM tst_solutions
2635  WHERE active_fi = %s
2636  AND question_fi = %s
2637  AND pass = %s
2638  AND step = %s
2639  AND authorized = %s
2640  ';
2641 
2642  return $this->db->manipulateF(
2643  $query,
2644  array('integer', 'integer', 'integer', 'integer', 'integer'),
2645  array($active_id, $this->getId(), $pass, $this->getStep(), (int) $authorized)
2646  );
2647  }
2648 
2649  $query = "
2650  DELETE FROM tst_solutions
2651  WHERE active_fi = %s
2652  AND question_fi = %s
2653  AND pass = %s
2654  AND authorized = %s
2655  ";
2656 
2657  return $this->db->manipulateF(
2658  $query,
2659  array('integer', 'integer', 'integer', 'integer'),
2660  array($active_id, $this->getId(), $pass, (int) $authorized)
2661  );
2662  }
2663 
2664  // fau: testNav - add timestamp as parameter to saveCurrentSolution
2665  public function saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized = true, $tstamp = 0): int
2666  {
2667  $next_id = $this->db->nextId("tst_solutions");
2668 
2669  $fieldData = array(
2670  "solution_id" => array("integer", $next_id),
2671  "active_fi" => array("integer", $active_id),
2672  "question_fi" => array("integer", $this->getId()),
2673  "value1" => array("clob", $value1),
2674  "value2" => array("clob", $value2),
2675  "pass" => array("integer", $pass),
2676  "tstamp" => array("integer", ((int) $tstamp > 0) ? (int) $tstamp : time()),
2677  'authorized' => array('integer', (int) $authorized)
2678  );
2679 
2680  if ($this->getStep() !== null) {
2681  $fieldData['step'] = array("integer", $this->getStep());
2682  }
2683 
2684  return $this->db->insert("tst_solutions", $fieldData);
2685  }
2686  // fau.
2687 
2688  public function updateCurrentSolution(int $solutionId, $value1, $value2, bool $authorized = true): int
2689  {
2690  $fieldData = array(
2691  "value1" => array("clob", $value1),
2692  "value2" => array("clob", $value2),
2693  "tstamp" => array("integer", time()),
2694  'authorized' => array('integer', (int) $authorized)
2695  );
2696 
2697  if ($this->getStep() !== null) {
2698  $fieldData['step'] = array("integer", $this->getStep());
2699  }
2700 
2701  return $this->db->update("tst_solutions", $fieldData, array(
2702  'solution_id' => array('integer', $solutionId)
2703  ));
2704  }
2705 
2706  // fau: testNav - added parameter to keep the timestamp (default: false)
2707  public function updateCurrentSolutionsAuthorization(int $activeId, int $pass, bool $authorized, bool $keepTime = false): int
2708  {
2709  $fieldData = array(
2710  'authorized' => array('integer', (int) $authorized)
2711  );
2712 
2713  if (!$keepTime) {
2714  $fieldData['tstamp'] = array('integer', time());
2715  }
2716 
2717  $whereData = array(
2718  'question_fi' => array('integer', $this->getId()),
2719  'active_fi' => array('integer', $activeId),
2720  'pass' => array('integer', $pass)
2721  );
2722 
2723  if ($this->getStep() !== null) {
2724  $whereData['step'] = array("integer", $this->getStep());
2725  }
2726 
2727  return $this->db->update('tst_solutions', $fieldData, $whereData);
2728  }
2729  // fau.
2730 
2731  // hey: prevPassSolutions - motivation slowly decreases on imagemap
2733 
2734  public static function implodeKeyValues(array $keyValues): string
2735  {
2736  return implode(assQuestion::KEY_VALUES_IMPLOSION_SEPARATOR, $keyValues);
2737  }
2738 
2739  public static function explodeKeyValues(string $keyValues): array
2740  {
2741  return explode(assQuestion::KEY_VALUES_IMPLOSION_SEPARATOR, $keyValues);
2742  }
2743 
2744  protected function deleteDummySolutionRecord(int $activeId, int $passIndex): void
2745  {
2746  foreach ($this->getSolutionValues($activeId, $passIndex, false) as $solutionRec) {
2747  if ($solutionRec['value1'] == '' && $solutionRec['value2'] == '') {
2748  $this->removeSolutionRecordById($solutionRec['solution_id']);
2749  }
2750  }
2751  }
2752 
2753  protected function isDummySolutionRecord(array $solutionRecord): bool
2754  {
2755  return !strlen($solutionRecord['value1']) && !strlen($solutionRecord['value2']);
2756  }
2757 
2758  protected function deleteSolutionRecordByValues(int $activeId, int $passIndex, bool $authorized, array $matchValues): void
2759  {
2760  $types = array("integer", "integer", "integer", "integer");
2761  $values = array($activeId, $this->getId(), $passIndex, (int) $authorized);
2762  $valuesCondition = [];
2763 
2764  foreach ($matchValues as $valueField => $value) {
2765  switch ($valueField) {
2766  case 'value1':
2767  case 'value2':
2768  $valuesCondition[] = "{$valueField} = %s";
2769  $types[] = 'text';
2770  $values[] = $value;
2771  break;
2772 
2773  default:
2774  throw new ilTestQuestionPoolException('invalid value field given: ' . $valueField);
2775  }
2776  }
2777 
2778  $valuesCondition = implode(' AND ', $valuesCondition);
2779 
2780  $query = "
2781  DELETE FROM tst_solutions
2782  WHERE active_fi = %s
2783  AND question_fi = %s
2784  AND pass = %s
2785  AND authorized = %s
2786  AND $valuesCondition
2787  ";
2788 
2789  if ($this->getStep() !== null) {
2790  $query .= " AND step = %s ";
2791  $types[] = 'integer';
2792  $values[] = $this->getStep();
2793  }
2794 
2795  $this->db->manipulateF($query, $types, $values);
2796  }
2797 
2798  protected function duplicateIntermediateSolutionAuthorized(int $activeId, int $passIndex): void
2799  {
2800  foreach ($this->getSolutionValues($activeId, $passIndex, false) as $rec) {
2801  $this->saveCurrentSolution($activeId, $passIndex, $rec['value1'], $rec['value2'], true, $rec['tstamp']);
2802  }
2803  }
2804 
2805  protected function forceExistingIntermediateSolution(int $activeId, int $passIndex, bool $considerDummyRecordCreation): void
2806  {
2807  $intermediateSolution = $this->getSolutionValues($activeId, $passIndex, false);
2808 
2809  if (!count($intermediateSolution)) {
2810  // make the authorized solution intermediate (keeping timestamps)
2811  // this keeps the solution_ids in synch with eventually selected in $_POST['deletefiles']
2812  $this->updateCurrentSolutionsAuthorization($activeId, $passIndex, false, true);
2813 
2814  // create a backup as authorized solution again (keeping timestamps)
2815  $this->duplicateIntermediateSolutionAuthorized($activeId, $passIndex);
2816 
2817  if ($considerDummyRecordCreation) {
2818  // create an additional dummy record to indicate the existence of an intermediate solution
2819  // even if all entries are deleted from the intermediate solution later
2820  $this->saveCurrentSolution($activeId, $passIndex, null, null, false);
2821  }
2822  }
2823  }
2824  // hey.
2825 
2829  public function setStep($step): void
2830  {
2831  $this->step = $step;
2832  }
2833 
2837  public function getStep(): ?int
2838  {
2839  return $this->step;
2840  }
2841 
2842  public static function convertISO8601FormatH_i_s_ExtendedToSeconds(string $time): int
2843  {
2844  $sec = 0;
2845  $time_array = explode(':', $time);
2846  if (count($time_array) == 3) {
2847  $sec += (int) $time_array[0] * 3600;
2848  $sec += (int) $time_array[1] * 60;
2849  $sec += (int) $time_array[2];
2850  }
2851  return $sec;
2852  }
2853 
2854  public function toJSON(): string
2855  {
2856  return json_encode([]);
2857  }
2858 
2859  abstract public function duplicate(bool $for_test = true, string $title = "", string $author = "", int $owner = -1, $testObjId = null): int;
2860 
2861  // hey: prevPassSolutions - check for authorized solution
2862  public function intermediateSolutionExists(int $active_id, int $pass): bool
2863  {
2864  $solutionAvailability = $this->lookupForExistingSolutions($active_id, $pass);
2865  return (bool) $solutionAvailability['intermediate'];
2866  }
2867 
2868  public function authorizedSolutionExists(int $active_id, ?int $pass): bool
2869  {
2870  if ($pass === null) {
2871  return false;
2872  }
2873  $solutionAvailability = $this->lookupForExistingSolutions($active_id, $pass);
2874  return (bool) $solutionAvailability['authorized'];
2875  }
2876 
2877  public function authorizedOrIntermediateSolutionExists(int $active_id, int $pass): bool
2878  {
2879  $solutionAvailability = $this->lookupForExistingSolutions($active_id, $pass);
2880  return $solutionAvailability['authorized'] || $solutionAvailability['intermediate'];
2881  }
2882  // hey.
2883 
2884  protected function lookupMaxStep(int $active_id, int $pass): int
2885  {
2886  $result = $this->db->queryF(
2887  "SELECT MAX(step) max_step FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s",
2888  array("integer", "integer", "integer"),
2889  array($active_id, $pass, $this->getId())
2890  );
2891 
2892  $row = $this->db->fetchAssoc($result);
2893 
2894  return (int) $row['max_step'];
2895  }
2896 
2897  // fau: testNav - new function lookupForExistingSolutions
2902  public function lookupForExistingSolutions(int $activeId, int $pass): array
2903  {
2904  $return = array(
2905  'authorized' => false,
2906  'intermediate' => false
2907  );
2908 
2909  $query = "
2910  SELECT authorized, COUNT(*) cnt
2911  FROM tst_solutions
2912  WHERE active_fi = %s
2913  AND question_fi = %s
2914  AND pass = %s
2915  ";
2916 
2917  if ($this->getStep() !== null) {
2918  $query .= " AND step = " . $this->db->quote((int) $this->getStep(), 'integer') . " ";
2919  }
2920 
2921  $query .= "
2922  GROUP BY authorized
2923  ";
2924 
2925  $result = $this->db->queryF($query, array('integer', 'integer', 'integer'), array($activeId, $this->getId(), $pass));
2926 
2927  while ($row = $this->db->fetchAssoc($result)) {
2928  if ($row['authorized']) {
2929  $return['authorized'] = $row['cnt'] > 0;
2930  } else {
2931  $return['intermediate'] = $row['cnt'] > 0;
2932  }
2933  }
2934  return $return;
2935  }
2936  // fau.
2937 
2938  public function isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue): bool
2939  {
2940  return false;
2941  }
2942 
2943  public function addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points): void
2944  {
2945  }
2946 
2947  public function removeAllExistingSolutions(): void
2948  {
2949  $query = "DELETE FROM tst_solutions WHERE question_fi = %s";
2950  $this->db->manipulateF($query, array('integer'), array($this->getId()));
2951  }
2952 
2953  public function removeExistingSolutions(int $activeId, int $pass): int
2954  {
2955  $query = "
2956  DELETE FROM tst_solutions
2957  WHERE active_fi = %s
2958  AND question_fi = %s
2959  AND pass = %s
2960  ";
2961 
2962  if ($this->getStep() !== null) {
2963  $query .= " AND step = " . $this->db->quote((int) $this->getStep(), 'integer') . " ";
2964  }
2965 
2966  return $this->db->manipulateF(
2967  $query,
2968  array('integer', 'integer', 'integer'),
2969  array($activeId, $this->getId(), $pass)
2970  );
2971  }
2972 
2973  public function resetUsersAnswer(int $activeId, int $pass): void
2974  {
2975  $this->removeExistingSolutions($activeId, $pass);
2976  $this->removeResultRecord($activeId, $pass);
2977 
2978  $this->log($activeId, "log_user_solution_willingly_deleted");
2979 
2980  $test = new ilObjTest(
2981  $this->test_id,
2982  false
2983  );
2984  $test->updateTestPassResults(
2985  $activeId,
2986  $pass,
2988  $this->getProcessLocker(),
2989  $this->getTestId()
2990  );
2991  }
2992 
2993  public function removeResultRecord(int $activeId, int $pass): int
2994  {
2995  $query = "
2996  DELETE FROM tst_test_result
2997  WHERE active_fi = %s
2998  AND question_fi = %s
2999  AND pass = %s
3000  ";
3001 
3002  if ($this->getStep() !== null) {
3003  $query .= " AND step = " . $this->db->quote((int) $this->getStep(), 'integer') . " ";
3004  }
3005 
3006  return $this->db->manipulateF(
3007  $query,
3008  array('integer', 'integer', 'integer'),
3009  array($activeId, $this->getId(), $pass)
3010  );
3011  }
3012 
3013  public function fetchValuePairsFromIndexedValues(array $indexedValues): array
3014  {
3015  $valuePairs = [];
3016 
3017  foreach ($indexedValues as $value1 => $value2) {
3018  $valuePairs[] = array('value1' => $value1, 'value2' => $value2);
3019  }
3020 
3021  return $valuePairs;
3022  }
3023 
3024  public function fetchIndexedValuesFromValuePairs(array $valuePairs): array
3025  {
3026  $indexedValues = [];
3027 
3028  foreach ($valuePairs as $valuePair) {
3029  $indexedValues[ $valuePair['value1'] ] = $valuePair['value2'];
3030  }
3031 
3032  return $indexedValues;
3033  }
3034 
3035  public function areObligationsToBeConsidered(): bool
3036  {
3038  }
3039 
3040  public function setObligationsToBeConsidered(bool $obligationsToBeConsidered): void
3041  {
3042  $this->obligationsToBeConsidered = $obligationsToBeConsidered;
3043  }
3044 
3045  public function updateTimestamp(): void
3046  {
3047  $this->db->manipulateF(
3048  "UPDATE qpl_questions SET tstamp = %s WHERE question_id = %s",
3049  array('integer', 'integer'),
3050  array(time(), $this->getId())
3051  );
3052  }
3053 
3054  // fau: testNav - new function getTestQuestionConfig()
3055  // hey: prevPassSolutions - get caching independent from configuration (config once)
3056  // renamed: getTestPresentationConfig() -> does the caching
3057  // completed: extracted instance building
3058  // avoids configuring cached instances on every access
3059  // allows a stable reconfigure of the instance from outside
3061 
3063  {
3064  if ($this->testQuestionConfigInstance === null) {
3065  $this->testQuestionConfigInstance = $this->buildTestPresentationConfig();
3066  }
3067 
3069  }
3070 
3078  {
3079  return new ilTestQuestionConfig();
3080  }
3081 
3084  {
3085  if (is_null($this->suggestedsolution_repo)) {
3086  $dic = ilQuestionPoolDIC::dic();
3087  $this->suggestedsolution_repo = $dic['question.repo.suggestedsolutions'];
3088  }
3090  }
3091 
3092  protected function loadSuggestedSolutions(): array
3093  {
3094  $question_id = $this->getId();
3095  return $this->getSuggestedSolutionsRepo()->selectFor($question_id);
3096  }
3097 
3112  public static function extendedTrim(string $value): string
3113  {
3114  return preg_replace(self::TRIM_PATTERN, '', $value);
3115  }
3116 
3117 }
static _replaceMediaObjectImageSrc(string $a_text, int $a_direction=0, string $nic='')
Replaces image source from mob image urls with the mob id or replaces mob id with the correct image s...
loadFromDb(int $question_id)
getSolutionValues($active_id, $pass=null, bool $authorized=true)
Loads solutions of a given user from the database an returns it.
const ADDITIONAL_CONTENT_EDITING_MODE_IPE
setNrOfTries(int $a_nr_of_tries)
ilTestQuestionConfig $testQuestionConfig
ilAssQuestionFeedback $feedbackOBJ
syncSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId)
ilGlobalPageTemplate $tpl
static _getManualScoringTypes()
Retrieve the manual scoring settings as type strings.
static getListByQuestionId($questionId)
instantiates a question hint list for the passed question id
deletePageOfQuestion(int $question_id)
$res
Definition: ltiservices.php:69
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
authorizedOrIntermediateSolutionExists(int $active_id, int $pass)
const MAXIMUM_THUMB_SIZE
bool $obligationsToBeConsidered
getActiveUserData(int $active_id)
Returns the user id and the test id for a given active id.
static _addLog( $user_id, $object_id, $logtext, $question_id=0, $original_id=0, $test_only=false, $test_ref_id=0)
Add an assessment log entry.
static _getObjectIDFromTestID($test_id)
Returns the ILIAS test object id for a given test id.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
migrateContentForLearningModule(ilAssSelfAssessmentMigrator $migrator)
toXML(bool $a_include_header=true, bool $a_include_binary=true, bool $a_shuffle=false, bool $test_output=false, bool $force_image_references=false)
Returns a QTI xml representation of the question.
static _isWriteable($object_id, $user_id)
Returns true, if the question pool is writeable by a given user.
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
static _getParticipantData($active_id)
Retrieves a participant name from active id.
const IL_INST_ID
Definition: constants.php:40
txtlng(string $a_module, string $a_topic, string $a_language)
gets the text for a given topic in a given language if the topic is not in the list, the topic itself with "-" will be returned
ILIAS HTTP Services $http
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getQuestionType()
Returns the question type of the question.
removeResultRecord(int $activeId, int $pass)
deleteAdditionalTableData(int $question_id)
deleteAnswers(int $question_id)
static _getSuggestedSolutionOutput(int $question_id)
updateCurrentSolutionsAuthorization(int $activeId, int $pass, bool $authorized, bool $keepTime=false)
buildTestPresentationConfig()
build basic test question configuration instance
resetUsersAnswer(int $activeId, int $pass)
Abstract basic class which is to be extended by the concrete assessment question type classes...
static isFileAvailable(string $file)
setProcessLocker(ilAssQuestionProcessLocker $processLocker)
copyPageOfQuestion(int $a_q_id)
Class ChatMainBarProvider .
static isObligationPossible(int $questionId)
setOwner(int $owner=-1)
persistWorkingState(int $active_id, $pass, bool $obligationsEnabled=false, bool $authorized=true)
persists the working state for current testactive and testpass
saveWorkingData(int $active_id, int $pass, bool $authorized=true)
Saves the learners input of the question to the database.
static _getAllReferences(int $id)
get all reference ids for object ID
ilAssQuestionProcessLocker $processLocker
static getImagePath(string $img, string $module_path="", string $mode="output", bool $offline=false)
get image path (for images located in a template directory)
ensureHintPageObjectExists($pageObjectId)
setSelfAssessmentEditingMode(bool $selfassessmenteditingmode)
getColumnCoord(int $a_col)
Get column "name" from number.
adjustReachedPointsByScoringOptions($points, $active_id, $pass=null)
Adjust the given reached points by checks for all special scoring options in the test container...
ensureNonNegativePoints($points)
SkillUsageService $skillUsageService
isDummySolutionRecord(array $solutionRecord)
bool $shuffle
Indicates whether the answers will be shuffled or not.
static _getInternalLinkHref(string $target="")
duplicate(bool $for_test=true, string $title="", string $author="", int $owner=-1, $testObjId=null)
__construct(string $title="", string $comment="", string $author="", int $owner=-1, string $question="")
assQuestion constructor
static _saveUsage(int $a_mob_id, string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
Save usage of mob within another container (e.g.
assQuestionSuggestedSolutionsDatabaseRepository $suggestedsolution_repo
getAdditionalTableName()
static isForcePassResultUpdateEnabled()
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static lookupParentObjId(int $questionId)
getImagePathWeb()
Returns the web image path for web accessable images of a question.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setThumbSize(int $a_size)
savePreviewData(ilAssQuestionPreviewSession $previewSession)
static deleteHintsByQuestionIds(array $questionIds)
Deletes all question hints relating to questions included in given question ids.
static getUsageOfObject(int $a_obj_id, bool $a_include_titles=false)
lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
static getNumExistingSolutionRecords(int $activeId, int $pass, int $questionId)
duplicateSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Definition: class.ilLog.php:30
static _cleanupMediaObjectUsage(string $a_text, string $a_usage_type, int $a_usage_id)
Synchronises appearances of media objects in $a_text with media object usage table.
importHint(int $question_id, array $hint_array)
static prepareFormOutput($a_str, bool $a_strip=false)
static setTokenMaxLifetimeInSeconds(int $token_max_lifetime_in_seconds)
setPreventRteUsage(bool $prevent_rte_usage)
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
getSuggestedSolution(int $subquestion_index=0)
static _getObjectIDFromActiveID($active_id)
Returns the ILIAS test object id for a given active id.
setComment(string $comment="")
persistPreviewState(ilAssQuestionPreviewSession $previewSession)
persists the preview state for current user and question
static _questionExistsInTest(int $question_id, int $test_id)
string $questionActionCmd
getValidAdditionalContentEditingModes()
float $points
The maximum available points for the question.
Customizing of pimple-DIC for ILIAS.
Definition: Container.php:35
static _lookupTestObjIdForQuestionId(int $q_id)
Get test Object ID for question ID.
int $obj_id
Object id of the container object.
fetchIndexedValuesFromValuePairs(array $valuePairs)
bool $selfassessmenteditingmode
$path
Definition: ltiservices.php:32
static implodeKeyValues(array $keyValues)
setExportImagePath(string $path)
static removeTrailingPathSeparators(string $path)
fixUnavailableSkinImageSources(string $html)
syncSuggestedSolutions(int $target_question_id, int $target_obj_id)
static _lookupObjId(int $ref_id)
static instantiateQuestionGUI(int $a_question_id)
isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue)
updateCurrentSolution(int $solutionId, $value1, $value2, bool $authorized=true)
static getASCIIFilename(string $a_filename)
deleteDummySolutionRecord(int $activeId, int $passIndex)
isPreviewSolutionCorrect(ilAssQuestionPreviewSession $previewSession)
global $DIC
Definition: feed.php:28
removeIntermediateSolution(int $active_id, int $pass)
setExportDetailsXLSX(ilAssExcelFormatHelper $worksheet, int $startrow, int $col, int $active_id, int $pass)
calculateReachedPoints($active_id, $pass=null, $authorizedSolution=true, $returndetails=false)
Returns the points, a learner has reached answering the question.
static isQuestionObligatory($question_id)
checks wether the question with given id is marked as obligatory or not
static _getUserIdFromActiveId(int $active_id)
static resetOriginalId(int $questionId)
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $previewSession)
resolveInternalLink(string $internal_link)
static instantiateQuestion(int $question_id)
Interface for html sanitizing functionality.
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
static _needsManualScoring(int $question_id)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
purifyAndPrepareTextAreaOutput(string $content)
setTestId(int $id=-1)
static http()
Fetches the global http state from ILIAS.
getImagePath($question_id=null, $object_id=null)
Returns the image path for web accessable images of a question.
duplicateComments(int $parent_source_id, int $source_id, int $parent_target_id, int $target_id)
buildHashedImageFilename(string $plain_image_filename, bool $unique=false)
static _lookupTitle(int $obj_id)
lookupForExistingSolutions(int $activeId, int $pass)
Lookup if an authorized or intermediate solution exists.
Transformation $shuffler
getAdjustedReachedPoints(int $active_id, int $pass, bool $authorizedSolution=true)
returns the reached points ...
static _getScoreCutting($active_id)
Determines if the score of a question should be cut at 0 points or the score of the whole test...
static _exists(string $a_parent_type, int $a_id, string $a_lang="", bool $a_no_cache=false)
Checks whether page exists.
static logAction(string $logtext, int $active_id, int $question_id)
removeSolutionRecordById(int $solutionId)
static _getReachedPoints(int $active_id, int $question_id, int $pass)
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
const MINIMUM_THUMB_SIZE
static _getIdForImportId(string $a_import_id)
get current object id for import id (static)
static _getSolutionMaxPass(int $question_id, int $active_id)
Returns the maximum pass a users question solution.
isComplete()
Returns true, if a question is complete for use.
intermediateSolutionExists(int $active_id, int $pass)
fromXML($item, int $questionpool_id, ?int $tst_id, &$tst_object, int &$question_counter, array $import_mapping, array &$solutionhints=[])
Receives parameters from a QTI parser and creates a valid ILIAS question object.
static _getCountSystem($active_id)
static $forcePassResultsUpdateEnabled
string $key
Consumer key/client ID value.
Definition: System.php:193
static getInstanceByType(string $type)
const CLIENT_WEB_DIR
Definition: constants.php:47
static _lookupAuthor($obj_id)
Gets the authors name of the ilObjTest object.
setPoints(float $points)
setObjId(int $obj_id=0)
string $question
The question text.
Basic GUI class for assessment questions.
static _exists(int $id, bool $reference=false, ?string $type=null)
log(int $active_id, string $langVar)
removeExistingSolutions(int $activeId, int $pass)
setDefaultNrOfTries(int $defaultnroftries)
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
isNonEmptyItemListPostSubmission(string $postSubmissionFieldname)
static saveOriginalId(int $questionId, int $originalId)
static _removeUsage(int $a_mob_id, string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
Remove usage of mob in another container.
const DEFAULT_THUMB_SIZE
static _updateQuestionCount(int $object_id)
ilAssQuestionLifecycle $lifecycle
$filename
Definition: buildRTE.php:78
ILIAS TestQuestionPool QuestionFilesService $questionFilesService
fetchValuePairsFromIndexedValues(array $indexedValues)
ILIAS Test TestParticipantInfoService $testParticipantInfo
createNewQuestion(bool $a_create_page=true)
Creates a new question without an owner when a new question is created This assures that an ID is giv...
static duplicateListForQuestion($originalQuestionId, $duplicateQuestionId)
duplicates a hint list from given original question id to given duplicate question id and returns an ...
int $test_id
The database id of a test in which the question is contained.
isAdditionalContentEditingModePageObject()
deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $previewSession, $reachedPoints)
setObligationsToBeConsidered(bool $obligationsToBeConsidered)
onDuplicate(int $originalParentId, int $originalQuestionId, int $duplicateParentId, int $duplicateQuestionId)
duplicateIntermediateSolutionAuthorized(int $activeId, int $passIndex)
const KEY_VALUES_IMPLOSION_SEPARATOR
copySuggestedSolutionFiles(int $source_questionpool_id, int $source_question_id)
saveQuestionDataToDb(int $original_id=-1)
syncSuggestedSolutionFiles(int $target_question_id, int $target_obj_id)
static $imageSourceFixReplaceMap
ilTestQuestionConfig $testQuestionConfigInstance
getSolutionMaxPass(int $active_id)
isValidAdditionalContentEditingMode(string $additionalContentEditingMode)
static extendedTrim(string $value)
Trim non-printable characters from the beginning and end of a string.
removeCurrentSolution(int $active_id, int $pass, bool $authorized=true)
array $suggested_solutions
lmMigrateQuestionTypeGenericContent(ilAssSelfAssessmentMigrator $migrator)
setFormattedExcelTitle($coordinates, $value)
ILIAS TestQuestionPool QuestionInfoService $questioninfo
setId(int $id=-1)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
ilDBInterface $db
onCopy(int $sourceParentId, int $sourceQuestionId, int $targetParentId, int $targetQuestionId)
ilObjUser $current_user
const ADDITIONAL_CONTENT_EDITING_MODE_RTE
setOriginalId(?int $original_id)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setExternalId(?string $external_id)
getTestOutputSolutions(int $activeId, int $pass)
setShuffler(Transformation $shuffler)
static convertISO8601FormatH_i_s_ExtendedToSeconds(string $time)
setTitle(string $title="")
deleteSolutionRecordByValues(int $activeId, int $passIndex, bool $authorized, array $matchValues)
A transformation is a function from one datatype to another.
setLastChange($lastChange)
getAnswerTableName()
static signFile(string $path_to_file)
authorizedSolutionExists(int $active_id, ?int $pass)
setLifecycle(ilAssQuestionLifecycle $lifecycle)
$message
Definition: xapiexit.php:32
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
static _isWriteable(int $question_id, int $user_id)
calculateResultsFromSolution(int $active_id, int $pass, bool $obligationsEnabled=false)
Calculates the question results from a previously saved question solution.
getReachedPoints(int $active_id, int $pass)
isAnswered(int $active_id, int $pass)
addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points)
forceExistingIntermediateSolution(int $activeId, int $passIndex, bool $considerDummyRecordCreation)
static _updateObjectiveResult(int $a_user_id, int $a_active_id, int $a_question_id)
const HAS_SPECIFIC_FEEDBACK
ILIAS DI LoggingServices $ilLog
lookupMaxStep(int $active_id, int $pass)
setAuthor(string $author="")
static prepareTextareaOutput(string $txt_output, bool $prepare_for_latex_output=false, bool $omitNl2BrWhenTextArea=false)
Prepares a string for a text area output where latex code may be in it If the text is HTML-free...
duplicateQuestionHints(int $originalQuestionId, int $duplicateQuestionId)
duplicateSuggestedSolutionFiles(int $parent_id, int $question_id)
Duplicates the files of a suggested solution if the question is duplicated.
string $additionalContentEditingMode
afterSyncWithOriginal(int $origQuestionId, int $dupQuestionId, int $origParentObjId, int $dupParentObjId)
$service
Definition: ltiservices.php:43
copySuggestedSolutions(int $target_question_id)
getSolutionRecordById(int $solutionId)
beforeSyncWithOriginal(int $origQuestionId, int $dupQuestionId, int $origParentObjId, int $dupParentObjId)
static _setReachedPoints(int $active_id, int $question_id, float $points, float $maxpoints, int $pass, bool $manualscoring, bool $obligationsEnabled, ?int $test_id=null)
Sets the points, a learner has reached answering the question Additionally objective results are upda...
setShuffle(?bool $shuffle=true)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
static explodeKeyValues(string $keyValues)
getUserSolutionPreferingIntermediate(int $active_id, $pass=null)
copyXHTMLMediaObjectsOfQuestion(int $a_q_id)
ILIAS Refinery Factory $refinery
static getFeedbackClassNameByQuestionType(string $questionType)
fixSvgToPng(string $imageFilenameContainingString)
setQuestion(string $question="")
string $export_image_path
(Web) Path to images
static setForcePassResultUpdateEnabled(bool $forcePassResultsUpdateEnabled)