ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
class.assFileUpload.php
Go to the documentation of this file.
1 <?php
2 
21 
22 require_once './Modules/Test/classes/inc.AssessmentConstants.php';
23 
36 {
37  // hey: prevPassSolutions - support reusing selected files
38  public const REUSE_FILES_TBL_POSTVAR = 'reusefiles';
39  public const DELETE_FILES_TBL_POSTVAR = 'deletefiles';
40  // hey.
41 
42  protected const HAS_SPECIFIC_FEEDBACK = false;
43  private \ILIAS\ResourceStorage\Services $irss;
44  private \ILIAS\FileDelivery\Services $file_delivery;
45  private \ILIAS\FileUpload\FileUpload $file_upload;
46 
47  protected ?int $maxsize = null;
48 
49  protected string $allowedextensions = '';
50 
51  private ?string $current_cmd;
52 
54  protected $completion_by_submission = false;
55 
69  public function __construct(
70  $title = '',
71  $comment = '',
72  $author = '',
73  $owner = -1,
74  $question = ''
75  ) {
78  global $DIC;
79  $this->irss = $DIC->resourceStorage();
80  $this->file_delivery = $DIC->fileDelivery();
81  $this->file_upload = $DIC->upload();
82  $this->current_cmd = $DIC['ilCtrl']->getCmd();
83  }
84 
90  public function isComplete(): bool
91  {
92  if (
93  strlen($this->title)
94  && ($this->author)
95  && ($this->question)
96  && ($this->getMaximumPoints() >= 0)
97  && is_numeric($this->getMaximumPoints())) {
98  return true;
99  }
100  return false;
101  }
102 
106  public function saveToDb($original_id = ''): void
107  {
108  if ($original_id == '') {
109  $this->saveQuestionDataToDb();
110  } else {
112  }
113 
115  parent::saveToDb();
116  }
117 
119  {
120  global $DIC;
121  $ilDB = $DIC['ilDB'];
122  $ilDB->manipulateF(
123  'DELETE FROM ' . $this->getAdditionalTableName() . ' WHERE question_fi = %s',
124  ['integer'],
125  [$this->getId()]
126  );
127  $ilDB->manipulateF(
128  'INSERT INTO ' . $this->getAdditionalTableName(
129  ) . ' (question_fi, maxsize, allowedextensions, compl_by_submission) VALUES (%s, %s, %s, %s)',
130  ['integer', 'float', 'text', 'integer' ],
131  [
132  $this->getId(),
133  $this->getMaxSize(),
134  (strlen($this->getAllowedExtensions())) ? $this->getAllowedExtensions() : null,
135  (int) $this->isCompletionBySubmissionEnabled()
136  ]
137  );
138  }
139 
145  public function loadFromDb($question_id): void
146  {
147  global $DIC;
148  $ilDB = $DIC['ilDB'];
149  $result = $ilDB->queryF(
150  'SELECT qpl_questions.*, ' . $this->getAdditionalTableName()
151  . '.* FROM qpl_questions LEFT JOIN ' . $this->getAdditionalTableName()
152  . ' ON ' . $this->getAdditionalTableName()
153  . '.question_fi = qpl_questions.question_id WHERE qpl_questions.question_id = %s',
154  ['integer'],
155  [$question_id]
156  );
157  if ($result->numRows() == 1) {
158  $data = $ilDB->fetchAssoc($result);
159  $this->setId($question_id);
160  $this->setTitle((string) $data['title']);
161  $this->setComment((string) $data['description']);
162  $this->setNrOfTries($data['nr_of_tries']);
163  $this->setOriginalId($data['original_id']);
164  $this->setObjId($data['obj_fi']);
165  $this->setAuthor($data['author']);
166  $this->setOwner($data['owner']);
167  $this->setPoints($data['points']);
168 
169  $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc((string) $data['question_text'], 1));
170  $this->setMaxSize(($data['maxsize'] ?? null) ? (int) $data['maxsize'] : null);
171  $this->setAllowedExtensions($data['allowedextensions'] ?? '');
172  $this->setCompletionBySubmission($data['compl_by_submission'] == 1 ? true : false);
173 
174  try {
175  $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle']));
178  }
179 
180  try {
181  $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
182  } catch (ilTestQuestionPoolException $e) {
183  }
184  }
185  parent::loadFromDb($question_id);
186  }
187 
191  public function duplicate(
192  bool $for_test = true,
193  string $title = '',
194  string $author = '',
195  int $owner = -1,
196  $testObjId = null
197  ): int {
198  if ($this->id <= 0) {
199  // The question has not been saved. It cannot be duplicated
200  return -1;
201  }
202  // duplicate the question in database
203  $this_id = $this->getId();
204  $thisObjId = $this->getObjId();
205 
206  $clone = $this;
207  $original_id = $this->questioninfo->getOriginalId($this->id);
208  $clone->id = -1;
209 
210  if ((int) $testObjId > 0) {
211  $clone->setObjId($testObjId);
212  }
213 
214  if ($title) {
215  $clone->setTitle($title);
216  }
217 
218  if ($author) {
219  $clone->setAuthor($author);
220  }
221  if ($owner) {
222  $clone->setOwner($owner);
223  }
224 
225  if ($for_test) {
226  $clone->saveToDb($original_id);
227  } else {
228  $clone->saveToDb();
229  }
230 
231  // copy question page content
232  $clone->copyPageOfQuestion($this_id);
233  // copy XHTML media objects
234  $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
235 
236  $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
237 
238  return $clone->id;
239  }
240 
244  public function copyObject($target_questionpool_id, $title = ''): int
245  {
246  if ($this->getId() <= 0) {
247  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
248  }
249  // duplicate the question in database
250  $clone = $this;
251  $original_id = $this->questioninfo->getOriginalId($this->id);
252  $clone->id = -1;
253  $source_questionpool_id = $this->getObjId();
254  $clone->setObjId($target_questionpool_id);
255  if ($title) {
256  $clone->setTitle($title);
257  }
258  $clone->saveToDb();
259 
260  // copy question page content
261  $clone->copyPageOfQuestion($original_id);
262  // copy XHTML media objects
263  $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
264 
265  $clone->onCopy($source_questionpool_id, $original_id, $clone->getObjId(), $clone->getId());
266 
267  return $clone->id;
268  }
269 
270  public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = ''): int
271  {
272  if ($this->getId() <= 0) {
273  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
274  }
275 
276  $sourceQuestionId = $this->id;
277  $sourceParentId = $this->getObjId();
278 
279  // duplicate the question in database
280  $clone = $this;
281  $clone->id = -1;
282 
283  $clone->setObjId($targetParentId);
284 
285  if ($targetQuestionTitle) {
286  $clone->setTitle($targetQuestionTitle);
287  }
288 
289  $clone->saveToDb();
290  // copy question page content
291  $clone->copyPageOfQuestion($sourceQuestionId);
292  // copy XHTML media objects
293  $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
294 
295  $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId());
296 
297  return $clone->id;
298  }
299 
305  public function getMaximumPoints(): float
306  {
307  return $this->getPoints();
308  }
309 
320  public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false): float
321  {
322  if ($returndetails) {
323  throw new ilTestException('return details not implemented for ' . __METHOD__);
324  }
325 
326  if ($this->isCompletionBySubmissionEnabled()) {
327  if (is_null($pass)) {
328  $pass = $this->getSolutionMaxPass($active_id);
329  }
330 
331  global $DIC;
332 
333  $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorizedSolution);
334 
335  while ($data = $DIC->database()->fetchAssoc($result)) {
336  if ($this->isDummySolutionRecord($data)) {
337  continue;
338  }
339 
340  return $this->getPoints();
341  }
342  }
343 
344  return 0.0;
345  }
346 
347  protected function calculateReachedPointsForSolution($userSolution)
348  {
349  if ($this->isCompletionBySubmissionEnabled() &&
350  is_array($userSolution) &&
351  count($userSolution)) {
352  return $this->getPoints();
353  }
354 
355  return 0;
356  }
357 
363  public function checkUpload(): bool
364  {
365  $this->lng->loadLanguageModule('form');
366 
367  foreach (
368  $this->file_upload->getResults() as $upload_result
369  ) { // only one supported at the moment, but we check all
370  if (!$upload_result->isOK()) {
371  $this->tpl->setOnScreenMessage('failure', $upload_result->getStatus()->getMessage(), true);
372  return false;
373  }
374 
375  // check file size
376  $size_bytes = $upload_result->getSize();
377  if ($size_bytes > $this->getMaxFilesizeInBytes()) {
378  $this->tpl->setOnScreenMessage('failure', $this->lng->txt('form_msg_file_size_exceeds'), true);
379  return false;
380  }
381 
382  // check suffixes
383  if (count($this->getAllowedExtensionsArray())) {
384  $filename_arr = pathinfo($upload_result->getName());
385  $suffix = $filename_arr['extension'] ?? '';
386  $mimetype = $upload_result->getMimeType();
387  if ($suffix === '') {
388  $this->tpl->setOnScreenMessage('failure', $this->lng->txt('form_msg_file_missing_file_ext'), true);
389  return false;
390  }
391 
392  if (!in_array(strtolower($suffix), $this->getAllowedExtensionsArray(), true)) {
393  $this->tpl->setOnScreenMessage('failure', $this->lng->txt('form_msg_file_wrong_file_type'), true);
394  return false;
395  }
396  }
397  // virus handling already done in upload-service
398  }
399  return true;
400  }
401 
405  public function getFileUploadPath($test_id, $active_id, $question_id = null): string
406  {
407  if (is_null($question_id)) {
408  $question_id = $this->getId();
409  }
410  return CLIENT_WEB_DIR . "/assessment/tst_{$test_id}/{$active_id}/{$question_id}/files/";
411  }
412 
416  protected function getPreviewFileUploadPath($userId): string
417  {
418  return CLIENT_WEB_DIR . "/assessment/qst_preview/{$userId}/{$this->getId()}/fileuploads/";
419  }
420 
426  public function getFileUploadPathWeb($test_id, $active_id, $question_id = null)
427  {
428  if (is_null($question_id)) {
429  $question_id = $this->getId();
430  }
432  . "/assessment/tst_{$test_id}/{$active_id}/{$question_id}/files/";
433  return str_replace(
434  ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH),
436  $webdir
437  );
438  }
439 
443  protected function getPreviewFileUploadPathWeb($userId)
444  {
446  . "/assessment/qst_preview/{$userId}/{$this->getId()}/fileuploads/";
447  return str_replace(
448  ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH),
450  $webdir
451  );
452  }
453 
459  public function getUploadedFiles($active_id, $pass = null, $authorized = true): array
460  {
461  global $DIC;
462  $ilDB = $DIC['ilDB'];
463 
464  if (is_null($pass)) {
465  $pass = $this->getSolutionMaxPass($active_id);
466  }
467  // fau: testNav - check existing value1 because the intermediate solution will have a dummy entry
468  $result = $ilDB->queryF(
469  'SELECT * FROM tst_solutions WHERE active_fi = %s '
470  . 'AND question_fi = %s AND pass = %s AND authorized = %s '
471  . 'AND value1 IS NOT NULL ORDER BY tstamp',
472  ['integer', 'integer', 'integer', 'integer'],
473  [$active_id, $this->getId(), $pass, (int) $authorized]
474  );
475  // fau.
476  $found = [];
477 
478  while ($data = $ilDB->fetchAssoc($result)) {
479  array_push($found, $data);
480  }
481 
482  return $found;
483  }
484 
485  public function getPreviewFileUploads(ilAssQuestionPreviewSession $previewSession): array
486  {
487  if ($previewSession->getParticipantsSolution() === false || $previewSession->getParticipantsSolution() === null) {
488  return [];
489  }
490 
491  return $previewSession->getParticipantsSolution();
492  }
493 
499  public function getUploadedFilesForWeb($active_id, $pass): array
500  {
501  $found = $this->getUploadedFiles($active_id, $pass);
502  $result = $this->db->queryF(
503  'SELECT test_fi FROM tst_active WHERE active_id = %s',
504  ['integer'],
505  [$active_id]
506  );
507  if ($result->numRows() == 1) {
508  $row = $this->db->fetchAssoc($result);
509  $test_id = $row['test_fi'];
510  $path = $this->getFileUploadPathWeb($test_id, $active_id);
511  foreach ($found as $idx => $data) {
512  // depending on whether the files are already stored in the IRSS or not, the files are compiled differently here.
513  // this can be removed with ILIAs 10 and switched exclusively to the IRSS variant.
514  // We recommend then to revise the whole handling of files
515 
516  if ($data['value2'] === 'rid') {
517  $rid = $this->irss->manage()->find($data['value1']);
518  if ($rid === null) {
519  continue;
520  }
521  $revision = $this->irss->manage()->getCurrentRevision($rid);
522  $stream = $this->irss->consume()->stream($rid)->getStream();
523  $url = $this->file_delivery->buildTokenURL(
524  $stream,
525  $revision->getTitle(),
526  Disposition::ATTACHMENT,
527  $this->current_user->getId(),
528  1
529  );
530 
531  $path = (string) $url;
532  $found[$idx]['webpath'] = $path;
533  $found[$idx]['value2'] = $revision->getTitle();
534  } else {
535  $found[$idx]['webpath'] = $path;
536  }
537  }
538  }
539  return $found;
540  }
541 
542  // fau: testNav new function deleteUnusedFiles()
549  protected function deleteUnusedFiles(array $rids_to_delete, $test_id, $active_id, $pass): void
550  {
551  // Remove Resources from IRSS
552  if ($rids_to_delete !== []) {
553  foreach ($rids_to_delete as $rid_to_delete) {
554  $rid_to_delete = $this->irss->manage()->find($rid_to_delete);
555  if ($rid_to_delete === null) {
556  continue;
557  }
558  $this->irss->manage()->remove(
559  $rid_to_delete,
561  );
562  }
563  }
564 
565  // Legacy implementation for not yet migrated files
566 
567  // read all solutions (authorized and intermediate) from all steps
568  $step = $this->getStep();
569  $this->setStep(null);
570  $solutions = array_merge(
571  $this->getSolutionValues($active_id, $pass, true),
572  $this->getSolutionValues($active_id, $pass, false)
573  );
574  $this->setStep($step);
575 
576  // get the used files from these solutions
577  $used_files = [];
578  foreach ($solutions as $solution) {
579  $used_files[] = $solution['value1'];
580  }
581 
582  // read the existing files for user and pass
583  // delete all files that are not used in the solutions
584  $uploadPath = $this->getFileUploadPath($test_id, $active_id);
585  if (is_dir($uploadPath) && is_readable($uploadPath)) {
586  $iter = new \RegexIterator(new \DirectoryIterator($uploadPath), '/^file_' . $active_id . '_' . $pass . '_(.*)/');
587  foreach ($iter as $file) {
589  if ($file->isFile() && !in_array($file->getFilename(), $used_files)) {
590  unlink($file->getPathname());
591  }
592  }
593  }
594  }
595  // fau.
596 
597  protected function deletePreviewFileUploads($userId, $userSolution, $files)
598  {
599  foreach ($files as $name) {
600  if (isset($userSolution[$name])) {
601  unset($userSolution[$name]);
602  @unlink($this->getPreviewFileUploadPath($userId) . $name);
603  }
604  }
605 
606  return $userSolution;
607  }
608 
609  public function getMaxFilesizeAsString(): string
610  {
611  $size = $this->getMaxFilesizeInBytes();
612  if ($size < 1024) {
613  return sprintf('%d Bytes', $size);
614  }
615 
616  if ($size < 1024 * 1024) {
617  return sprintf('%.1f KB', $size / 1024);
618  }
619 
620  return sprintf('%.1f MB', $size / 1024 / 1024);
621  }
622 
623  protected function getMaxFilesizeInBytes(): int
624  {
625  if ($this->getMaxSize() > 0) {
626  return $this->getMaxSize();
627  }
628 
629  return $this->determineMaxFilesize();
630  }
631 
632 
633  public function determineMaxFilesize(): int
634  {
635  $upload_max_filesize = ini_get('upload_max_filesize');
636  $post_max_size = ini_get('post_max_size');
637 
638  //convert from short-string representation to "real" bytes
639  $multiplier_a = [ "K" => 1024, "M" => 1024 * 1024, "G" => 1024 * 1024 * 1024 ];
640  $umf_parts = preg_split(
641  "/(\d+)([K|G|M])/",
642  $upload_max_filesize,
643  -1,
644  PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
645  );
646  $pms_parts = preg_split(
647  "/(\d+)([K|G|M])/",
648  $post_max_size,
649  -1,
650  PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
651  );
652 
653  if (count($umf_parts) === 2) {
654  $upload_max_filesize = $umf_parts[0] * $multiplier_a[$umf_parts[1]];
655  }
656 
657  if (count($pms_parts) === 2) {
658  $post_max_size = $pms_parts[0] * $multiplier_a[$pms_parts[1]];
659  }
660 
661  // use the smaller one as limit
662  $max_filesize = min($upload_max_filesize, $post_max_size);
663 
664  if (!$max_filesize) {
665  $max_filesize = max($upload_max_filesize, $post_max_size);
666  return $max_filesize;
667  }
668 
669  return $max_filesize;
670  }
671 
677  public function saveWorkingData($active_id, $pass = null, $authorized = true): bool
678  {
679  if ($pass === null || $pass < 0) {
680  $pass = \ilObjTest::_getPass($active_id);
681  }
682 
683  $test_id = $this->testParticipantInfo->lookupTestIdByActiveId($active_id);
684 
685  try {
686  $upload_handling_required = $this->current_cmd !== 'submitSolution'
687  && $this->current_cmd !== 'showInstantResponse'
688  && !$this->isFileDeletionAction()
689  && $this->isFileUploadAvailable()
690  && $this->checkUpload();
691  } catch (IllegalStateException $e) {
692  $this->tpl->setOnScreenMessage('failure', $e->getMessage(), true);
693  return false;
694  }
695 
696  $rid = null;
697 
698  if ($upload_handling_required) {
699  // upload new file to storage
700  $upload_results = $this->file_upload->getResults();
701  $upload_result = end($upload_results); // only one supported at the moment
702  $rid = $this->irss->manage()->upload(
703  $upload_result,
705  );
706  }
707 
708  $entered_values = false;
709 
710  // RIDS to delete
711  // Unfortunately, at the moment it is not possible to delete the files from the IRSS, because the process takes
712  // place within the ProcessLocker and the IRSS tables cannot be used. we have to remove them after the lock.
713  // therefore we store the rids to delete in an array for later deletion.
714 
715  $rids_to_delete = $this->resolveRIDStoDelete();
716 
717  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
718  function () use (
719  &$entered_values,
720  $upload_handling_required,
721  $test_id,
722  $active_id,
723  $pass,
724  $authorized,
725  $rid
726  ) {
727  if ($authorized === false) {
728  $this->forceExistingIntermediateSolution($active_id, $pass, true);
729  }
730 
731  if ($this->isFileDeletionAction()) {
732  if ($this->isFileDeletionSubmitAvailable()) {
733  foreach ($_POST[self::DELETE_FILES_TBL_POSTVAR] as $solution_id) {
734  $this->removeSolutionRecordById($solution_id);
735  }
736  } else {
737  $this->tpl->setOnScreenMessage('info', $this->lng->txt('no_checkbox'), true);
738  }
739  } else {
740  if ($this->isFileReuseHandlingRequired()) {
741  foreach ($_POST[self::REUSE_FILES_TBL_POSTVAR] as $solutionId) {
742  $solution = $this->getSolutionRecordById($solutionId);
743 
744  $this->saveCurrentSolution(
745  $active_id,
746  $pass,
747  $solution['value1'],
748  $solution['value2'],
749  false,
750  $solution['tstamp']
751  );
752  }
753  }
754 
755  if ($upload_handling_required && $rid !== null) {
756 
757  $revision = $this->irss->manage()->getCurrentRevision($rid);
758 
759  $this->saveCurrentSolution(
760  $active_id,
761  $pass,
762  $rid->serialize(),
763  'rid',
764  false,
765  time()
766  );
767 
768  $entered_values = true;
769  }
770  }
771 
772  if ($authorized === true && $this->intermediateSolutionExists($active_id, $pass)) {
773  // remove the dummy record of the intermediate solution
774  $this->deleteDummySolutionRecord($active_id, $pass);
775 
776  // delete the authorized solution and make the intermediate solution authorized (keeping timestamps)
777  $this->removeCurrentSolution($active_id, $pass, true);
778  $this->updateCurrentSolutionsAuthorization($active_id, $pass, true, true);
779  }
780 
781  }
782  );
783 
784  $this->deleteUnusedFiles(
785  $rids_to_delete,
786  $test_id,
787  $active_id,
788  $pass
789  );
790 
791  if ($entered_values) {
793  assQuestion::logAction($this->lng->txtlng(
794  'assessment',
795  'log_user_entered_values',
797  ), $active_id, $this->getId());
798  }
799  } else {
801  assQuestion::logAction($this->lng->txtlng(
802  'assessment',
803  'log_user_not_entered_values',
805  ), $active_id, $this->getId());
806  }
807  }
808 
809  return true;
810  }
811 
812  protected function resolveRIDStoDelete(): array
813  {
814  $rids_to_delete = [];
815  if ($this->isFileDeletionAction() && $this->isFileDeletionSubmitAvailable()) {
816  $res = $this->db->query(
817  "SELECT value1 FROM tst_solutions WHERE value2 = 'rid' AND " . $this->db->in(
818  'solution_id',
819  $_POST[self::DELETE_FILES_TBL_POSTVAR],
820  false,
821  'integer'
822  )
823  );
824  while ($d = $this->db->fetchAssoc($res)) {
825  $rids_to_delete[] = $d['value1'];
826  }
827  }
828  return $rids_to_delete;
829  }
830 
831  protected function removeSolutionRecordById(int $solution_id): int
832  {
833  return parent::removeSolutionRecordById($solution_id);
834  }
835 
840  public function getUserSolutionPreferingIntermediate($active_id, $pass = null): array
841  {
842  $solution = $this->getSolutionValues($active_id, $pass, false);
843 
844  if (!count($solution)) {
845  $solution = $this->getSolutionValues($active_id, $pass, true);
846  } else {
847  $cleaned = [];
848  foreach ($solution as $row) {
849  if (!empty($row['value1'])) {
850  $cleaned[] = $row;
851  }
852  }
853  $solution = $cleaned;
854  }
855 
856  return $solution;
857  }
858  // fau.
859 
860  public function removeIntermediateSolution(int $active_id, int $pass): void
861  {
862  parent::removeIntermediateSolution($active_id, $pass);
863 
864  $test_id = $this->testParticipantInfo->lookupTestIdByActiveId($active_id);
865  if ($test_id !== -1) {
866  // TODO: This can be removed with ILIAS 10
867  $this->deleteUnusedFiles([], $test_id, $active_id, $pass);
868  }
869  }
870 
871 
872  protected function savePreviewData(ilAssQuestionPreviewSession $previewSession): void
873  {
874  $userSolution = $previewSession->getParticipantsSolution();
875 
876  if (!is_array($userSolution)) {
877  $userSolution = [];
878  }
879 
880  // hey: prevPassSolutions - readability spree - get a chance to understand the code
881  if ($this->isFileDeletionAction()) {
882  // hey.
883  // hey: prevPassSolutions - readability spree - get a chance to understand the code
884  if ($this->isFileDeletionSubmitAvailable()) {
885  // hey.
886  $userSolution = $this->deletePreviewFileUploads($previewSession->getUserId(), $userSolution, $_POST['deletefiles']);
887  } else {
888  $this->tpl->setOnScreenMessage('info', $this->lng->txt('no_checkbox'), true);
889  }
890  } else {
891  // hey: prevPassSolutions - readability spree - get a chance to understand the code
892  try {
893  $fileUploadAvailable = $this->current_cmd !== 'instantResponse'
894  && $this->isFileUploadAvailable();
895  } catch (IllegalStateException $e) {
896  $this->tpl->setOnScreenMessage('failure', $e->getMessage(), true);
897  return;
898  }
899  if ($fileUploadAvailable) {
900  // hey.
901  if ($this->checkUpload()) {
902  if (!@file_exists($this->getPreviewFileUploadPath($previewSession->getUserId()))) {
904  }
905 
906  $version = time();
907  $filename_arr = pathinfo($_FILES['upload']['name']);
908  $extension = $filename_arr['extension'];
909  $newfile = 'file_' . md5($_FILES['upload']['name']) . '_' . $version . '.' . $extension;
911  $_FILES['upload']['tmp_name'],
912  $_FILES['upload']['name'],
913  $this->getPreviewFileUploadPath($previewSession->getUserId()) . $newfile
914  );
915 
916  $userSolution[$newfile] = [
917  'solution_id' => $newfile,
918  'value1' => $newfile,
919  'value2' => $_FILES['upload']['name'],
920  'tstamp' => $version,
921  'webpath' => $this->getPreviewFileUploadPathWeb($previewSession->getUserId())
922  ];
923  }
924  }
925  }
926 
927  $previewSession->setParticipantsSolution($userSolution);
928  }
929 
930  protected function handleSubmission(int $active_id, int $pass, bool $obligations_answered, bool $authorized): void
931  {
932  if (!$authorized
934  || !$this->getUploadedFiles($active_id, $pass, $authorized)) {
935  return;
936  }
937 
938  $maxpoints = $this->questioninfo->getMaximumPoints($this->getId());
939 
940  $points = $maxpoints;
941 
942  assQuestion::_setReachedPoints($active_id, $this->getId(), $points, $maxpoints, $pass, true, $obligations_answered);
943 
945  ilObjTest::_getObjectIDFromActiveID((int) $active_id),
946  ilObjTestAccess::_getParticipantId((int) $active_id)
947  );
948  }
949 
950  public function getQuestionType(): string
951  {
952  return 'assFileUpload';
953  }
954 
955  public function getAdditionalTableName(): string
956  {
957  return 'qpl_qst_fileupload';
958  }
959 
960  public function getAnswerTableName(): string
961  {
962  return '';
963  }
964 
968  public function deleteAnswers($question_id): void
969  {
970  }
971 
976  public function getRTETextWithMediaObjects(): string
977  {
978  return parent::getRTETextWithMediaObjects();
979  }
980 
984  public function setExportDetailsXLSX(ilAssExcelFormatHelper $worksheet, int $startrow, int $col, int $active_id, int $pass): int
985  {
986  parent::setExportDetailsXLSX($worksheet, $startrow, $col, $active_id, $pass);
987 
988  $i = 1;
989  $solutions = $this->getSolutionValues($active_id, $pass);
990  foreach ($solutions as $solution) {
991  $worksheet->setCell($startrow + $i, $col, $this->lng->txt('result'));
992  $worksheet->setBold($worksheet->getColumnCoord($col) . ($startrow + $i));
993  if (strlen($solution['value1'])) {
994  $worksheet->setCell($startrow + $i, $col + 2, $solution['value1']);
995  $worksheet->setCell($startrow + $i, $col + 3, $solution['value2']);
996  }
997  $i++;
998  }
999 
1000  return $startrow + $i + 1;
1001  }
1002 
1015  public function fromXML($item, int $questionpool_id, ?int $tst_id, &$tst_object, int &$question_counter, array $import_mapping, array &$solutionhints = []): array
1016  {
1017  $import = new assFileUploadImport($this);
1018  return $import->fromXML($item, $questionpool_id, $tst_id, $tst_object, $question_counter, $import_mapping);
1019  }
1020 
1021  public function toXML($a_include_header = true, $a_include_binary = true, $a_shuffle = false, $test_output = false, $force_image_references = false): string
1022  {
1023  $export = new assFileUploadExport($this);
1024  return $export->toXML($a_include_header, $a_include_binary, $a_shuffle, $test_output, $force_image_references);
1025  }
1026 
1027  public function getBestSolution($active_id, $pass): array
1028  {
1029  $user_solution = [];
1030  return $user_solution;
1031  }
1032 
1033  public function getMaxSize(): ?int
1034  {
1035  return $this->maxsize;
1036  }
1037 
1038  public function setMaxSize(?int $value): void
1039  {
1040  $this->maxsize = $value;
1041  }
1042 
1043  public function getAllowedExtensionsArray(): array
1044  {
1045  if ($this->allowedextensions === '') {
1046  return [];
1047  }
1048 
1049  return array_filter(array_map('trim', explode(',', $this->allowedextensions)));
1050  }
1051 
1052  public function getAllowedExtensions(): string
1053  {
1054  return $this->allowedextensions;
1055  }
1056 
1057  public function setAllowedExtensions(string $a_value): void
1058  {
1059  $this->allowedextensions = strtolower(trim($a_value));
1060  }
1061 
1062  public function hasFileUploads(int $test_id): bool
1063  {
1064  $query = '
1065  SELECT tst_solutions.solution_id
1066  FROM tst_solutions, tst_active, qpl_questions
1067  WHERE tst_solutions.active_fi = tst_active.active_id
1068  AND tst_solutions.question_fi = qpl_questions.question_id
1069  AND tst_solutions.question_fi = %s AND tst_active.test_fi = %s
1070  AND tst_solutions.value1 is not null';
1071  $result = $this->db->queryF(
1072  $query,
1073  ['integer', 'integer'],
1074  [$this->getId(), $test_id]
1075  );
1076  if ($result->numRows() > 0) {
1077  return true;
1078  }
1079 
1080  return false;
1081  }
1082 
1083  public function deliverFileUploadZIPFile(int $ref_id, int $test_id, string $test_title): void
1084  {
1085  $exporter = new ilAssFileUploadUploadsExporter(
1086  $this->db,
1087  $this->lng,
1088  $ref_id,
1089  $test_id
1090  );
1091 
1092  $exporter->setTestTitle($test_title);
1093  $exporter->setQuestion($this);
1094 
1095  $exporter->buildAndDownload();
1096  }
1097 
1098  public function isCompletionBySubmissionEnabled(): bool
1099  {
1101  }
1102 
1103  public function setCompletionBySubmission(bool $bool): assFileUpload
1104  {
1105  $this->completion_by_submission = (bool) $bool;
1106  return $this;
1107  }
1108 
1109  public static function isObligationPossible(int $question_id): bool
1110  {
1111  return true;
1112  }
1113 
1115  {
1116  return parent::buildTestPresentationConfig()
1118  }
1119 
1120  protected function isFileDeletionAction(): bool
1121  {
1123  }
1124 
1125  protected function isFileDeletionSubmitAvailable(): bool
1126  {
1127  return $this->isNonEmptyItemListPostSubmission(self::DELETE_FILES_TBL_POSTVAR);
1128  }
1129 
1130  protected function isFileReuseSubmitAvailable(): bool
1131  {
1132  return $this->isNonEmptyItemListPostSubmission(self::REUSE_FILES_TBL_POSTVAR);
1133  }
1134 
1135  protected function isFileReuseHandlingRequired(): bool
1136  {
1137  if (!$this->getTestPresentationConfig()->isPreviousPassSolutionReuseAllowed()) {
1138  return false;
1139  }
1140 
1141  if (!$this->isFileReuseSubmitAvailable()) {
1142  return false;
1143  }
1144 
1145  return true;
1146  }
1147 
1151  protected function isFileUploadAvailable(): bool
1152  {
1153  if (!$this->file_upload->hasBeenProcessed()) {
1154  $this->file_upload->process();
1155  }
1156  return $this->file_upload->hasUploads();
1157  }
1158 }
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...
getPreviewFileUploadPathWeb($userId)
Returns the filesystem path for file uploads.
calculateReachedPoints($active_id, $pass=null, $authorizedSolution=true, $returndetails=false)
Returns the points, a learner has reached answering the question.
getSolutionValues($active_id, $pass=null, bool $authorized=true)
Loads solutions of a given user from the database an returns it.
setFormChangeDetectionEnabled($enableFormChangeDetection)
Set if the detection of form changes is enabled.
ILIAS FileUpload FileUpload $file_upload
setNrOfTries(int $a_nr_of_tries)
$res
Definition: ltiservices.php:69
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
savePreviewData(ilAssQuestionPreviewSession $previewSession)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
saveWorkingData($active_id, $pass=null, $authorized=true)
public
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
static _getParticipantId($active_id)
Get user id for active id.
getBestSolution($active_id, $pass)
updateCurrentSolutionsAuthorization(int $activeId, int $pass, bool $authorized, bool $keepTime=false)
Abstract basic class which is to be extended by the concrete assessment question type classes...
setOwner(int $owner=-1)
fromXML($item, int $questionpool_id, ?int $tst_id, &$tst_object, int &$question_counter, array $import_mapping, array &$solutionhints=[])
Creates a question from a QTI file.
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
getColumnCoord(int $a_col)
Get column "name" from number.
isDummySolutionRecord(array $solutionRecord)
static isObligationPossible(int $question_id)
__construct(string $title="", string $comment="", string $author="", int $owner=-1, string $question="")
assQuestion constructor
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
isComplete()
Returns true, if the question is complete for use.
createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle='')
setMaxSize(?int $value)
deletePreviewFileUploads($userId, $userSolution, $files)
setCell($a_row, $a_col, $a_value, $datatype=null)
getFileUploadPathWeb($test_id, $active_id, $question_id=null)
Returns the file upload path for web accessible files of a question.
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
static _getObjectIDFromActiveID($active_id)
Returns the ILIAS test object id for a given active id.
setComment(string $comment="")
Class for file upload questions.
float $points
The maximum available points for the question.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Base Exception for all Exceptions relating to Modules/Test.
$path
Definition: ltiservices.php:32
static removeTrailingPathSeparators(string $path)
deleteDummySolutionRecord(int $activeId, int $passIndex)
getMaximumPoints()
Returns the maximum points, a learner can reach answering the question.
global $DIC
Definition: feed.php:28
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
setBold(string $a_coords)
Set cell(s) to bold.
$ref_id
Definition: ltiauth.php:67
__construct(VocabulariesInterface $vocabularies)
getUserSolutionPreferingIntermediate($active_id, $pass=null)
setCompletionBySubmission(bool $bool)
static logAction(string $logtext, int $active_id, int $question_id)
deliverFileUploadZIPFile(int $ref_id, int $test_id, string $test_title)
checkUpload()
Check file upload.
calculateReachedPointsForSolution($userSolution)
hasFileUploads(int $test_id)
intermediateSolutionExists(int $active_id, int $pass)
getPreviewFileUploads(ilAssQuestionPreviewSession $previewSession)
ILIAS FileDelivery Services $file_delivery
$url
Definition: ltiregstart.php:35
const CLIENT_WEB_DIR
Definition: constants.php:47
static moveUploadedFile(string $a_file, string $a_name, string $a_target, bool $a_raise_errors=true, string $a_mode="move_uploaded")
move uploaded file
removeIntermediateSolution(int $active_id, int $pass)
setPoints(float $points)
setObjId(int $obj_id=0)
saveAdditionalQuestionDataToDb()
Saves a record to the question types additional data table.
string $question
The question text.
isNonEmptyItemListPostSubmission(string $postSubmissionFieldname)
getUploadedFilesForWeb($active_id, $pass)
Returns the web accessible uploaded files for an active user in a given pass.
int $test_id
The database id of a test in which the question is contained.
handleSubmission(int $active_id, int $pass, bool $obligations_answered, bool $authorized)
setExportDetailsXLSX(ilAssExcelFormatHelper $worksheet, int $startrow, int $col, int $active_id, int $pass)
{}
toXML($a_include_header=true, $a_include_binary=true, $a_shuffle=false, $test_output=false, $force_image_references=false)
saveQuestionDataToDb(int $original_id=-1)
getPreviewFileUploadPath($userId)
Returns the filesystem path for file uploads.
getSolutionMaxPass(int $active_id)
ILIAS ResourceStorage Services $irss
removeCurrentSolution(int $active_id, int $pass, bool $authorized=true)
getFileUploadPath($test_id, $active_id, $question_id=null)
Returns the filesystem path for file uploads.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setId(int $id=-1)
copyObject($target_questionpool_id, $title='')
Copies an assFileUpload object.
setOriginalId(?int $original_id)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setTitle(string $title="")
removeSolutionRecordById(int $solution_id)
setLifecycle(ilAssQuestionLifecycle $lifecycle)
loadFromDb($question_id)
Loads a assFileUpload object from a database.
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
getUploadedFiles($active_id, $pass=null, $authorized=true)
Returns the uploaded files for an active user in a given pass.
forceExistingIntermediateSolution(int $activeId, int $passIndex, bool $considerDummyRecordCreation)
setAuthor(string $author="")
duplicate(bool $for_test=true, string $title='', string $author='', int $owner=-1, $testObjId=null)
Duplicates an assFileUpload.
getSolutionRecordById(int $solutionId)
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...
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
saveToDb($original_id='')
Saves a assFileUpload object to a database.
$version
Definition: plugin.php:24
deleteAnswers($question_id)
setAllowedExtensions(string $a_value)
static _updateStatus(int $a_obj_id, int $a_usr_id, ?object $a_obj=null, bool $a_percentage=false, bool $a_force_raise=false)
setQuestion(string $question="")