19declare(strict_types=1);
46 private \ILIAS\ResourceStorage\Services
$irss;
82 $this->irss =
$DIC->resourceStorage();
83 $this->file_delivery =
$DIC->fileDelivery();
84 $this->file_upload =
$DIC[
'upload'];
85 $this->current_cmd =
$DIC[
'ilCtrl']->getCmd();
86 $local_dic = QuestionPoolDIC::dic();
87 $this->participant_repository = $local_dic[
'participant_repository'];
120 $this->db->manipulateF(
125 $this->db->manipulateF(
127 ) .
' (question_fi, maxsize, allowedextensions, compl_by_submission) VALUES (%s, %s, %s, %s)',
128 [
'integer',
'float',
'text',
'integer' ],
141 $result = $this->db->queryF(
145 .
'.question_fi = qpl_questions.question_id WHERE qpl_questions.question_id = %s',
149 if ($result->numRows() == 1) {
150 $data = $this->db->fetchAssoc($result);
151 $this->
setId($question_id);
153 $this->
setComment((
string) $data[
'description']);
162 $this->
setMaxSize(($data[
'maxsize'] ??
null) ? (
int)
$data[
'maxsize'] :
null);
177 parent::loadFromDb($question_id);
188 bool $authorized_solution =
true
194 if ($pass ===
null) {
200 while (
$data = $this->db->fetchAssoc($result)) {
213 if ($this->isCompletionBySubmissionEnabled()
214 && is_array($user_solution)
215 && $user_solution !== []) {
216 return $this->getPoints();
229 $this->
lng->loadLanguageModule(
'form');
232 $this->file_upload->getResults() as $upload_result
234 if (!$upload_result->isOK()) {
235 $this->tpl->setOnScreenMessage(
'failure', $upload_result->getStatus()->getMessage(),
true);
240 $size_bytes = $upload_result->getSize();
241 if ($size_bytes > $this->getMaxFilesizeInBytes()) {
242 $this->tpl->setOnScreenMessage(
'failure', $this->
lng->txt(
'form_msg_file_size_exceeds'),
true);
247 if (count($this->getAllowedExtensionsArray())) {
248 $filename_arr = pathinfo($upload_result->getName());
249 $suffix = $filename_arr[
'extension'] ??
'';
250 $mimetype = $upload_result->getMimeType();
251 if ($suffix ===
'') {
252 $this->tpl->setOnScreenMessage(
'failure', $this->
lng->txt(
'form_msg_file_missing_file_ext'),
true);
256 if (!in_array(strtolower($suffix), $this->getAllowedExtensionsArray(),
true)) {
257 $this->tpl->setOnScreenMessage(
'failure', $this->
lng->txt(
'form_msg_file_wrong_file_type'),
true);
271 if (is_null($question_id)) {
272 $question_id = $this->
getId();
274 return CLIENT_WEB_DIR .
"/assessment/tst_{$test_id}/{$active_id}/{$question_id}/files/";
282 return CLIENT_WEB_DIR .
"/assessment/qst_preview/{$userId}/{$this->getId()}/fileuploads/";
292 if (is_null($question_id)) {
293 $question_id = $this->
getId();
296 .
"/assessment/tst_{$test_id}/{$active_id}/{$question_id}/files/";
307 .
"/assessment/qst_preview/{$user_id}/{$this->getId()}/fileuploads/";
318 bool $authorized =
true
320 if (is_null($pass)) {
321 $pass = $this->getSolutionMaxPass($active_id);
324 $result = $this->db->queryF(
325 'SELECT * FROM tst_solutions WHERE active_fi = %s '
326 .
'AND question_fi = %s AND pass = %s AND authorized = %s '
327 .
'AND value1 IS NOT NULL ORDER BY tstamp',
328 [
'integer',
'integer',
'integer',
'integer'],
329 [$active_id, $this->
getId(), $pass, (
int) $authorized]
334 while (
$data = $this->db->fetchAssoc($result)) {
335 array_push($found,
$data);
357 $found = $this->getUploadedFiles($active_id, $pass);
358 $result = $this->db->queryF(
359 'SELECT test_fi FROM tst_active WHERE active_id = %s',
363 if ($result->numRows() == 1) {
364 $row = $this->db->fetchAssoc($result);
365 $test_id = $row[
'test_fi'];
366 $path = $this->getFileUploadPathWeb($test_id, $active_id);
367 foreach ($found as $idx =>
$data) {
372 if (
$data[
'value2'] ===
'rid') {
373 $rid = $this->irss->manage()->find(
$data[
'value1']);
377 $revision = $this->irss->manage()->getCurrentRevision($rid);
378 $stream = $this->irss->consume()->stream($rid)->getStream();
379 $url = $this->file_delivery->buildTokenURL(
381 $revision->getTitle(),
382 Disposition::ATTACHMENT,
383 $this->current_user->getId(),
388 $found[$idx][
'webpath'] =
$path;
389 $found[$idx][
'value2'] = $revision->getTitle();
391 $found[$idx][
'webpath'] =
$path;
405 protected function deleteUnusedFiles(array $rids_to_delete, $test_id, $active_id, $pass): void
408 if ($rids_to_delete !== []) {
409 foreach ($rids_to_delete as $rid_to_delete) {
410 $rid_to_delete = $this->irss->manage()->find($rid_to_delete);
411 if ($rid_to_delete ===
null) {
414 $this->irss->manage()->remove(
424 $step = $this->getStep();
425 $this->setStep(
null);
426 $solutions = array_merge(
427 $this->getSolutionValues($active_id, $pass,
true),
428 $this->getSolutionValues($active_id, $pass,
false)
430 $this->setStep($step);
434 foreach ($solutions as $solution) {
435 $used_files[] = $solution[
'value1'];
440 $uploadPath = $this->getFileUploadPath($test_id, $active_id);
441 if (is_dir($uploadPath) && is_readable($uploadPath)) {
442 $iter = new \RegexIterator(
new \DirectoryIterator($uploadPath),
'/^file_' . $active_id .
'_' . $pass .
'_(.*)/');
443 foreach ($iter as $file) {
445 if ($file->isFile() && !in_array($file->getFilename(), $used_files)) {
446 unlink($file->getPathname());
455 foreach ($files as $name) {
456 if (isset($userSolution[$name])) {
457 unset($userSolution[$name]);
458 @unlink($this->getPreviewFileUploadPath($userId) . $name);
462 return $userSolution;
467 $size = $this->getMaxFilesizeInBytes();
469 return sprintf(
'%d Bytes', $size);
472 if ($size < 1024 * 1024) {
473 return sprintf(
'%.1f KB', $size / 1024);
476 return sprintf(
'%.1f MB', $size / 1024 / 1024);
481 if ($this->getMaxSize() > 0) {
482 return $this->getMaxSize();
485 return $this->determineMaxFilesize();
491 $upload_max_filesize = ini_get(
'upload_max_filesize');
492 $post_max_size = ini_get(
'post_max_size');
495 $multiplier_a = [
"K" => 1024,
"M" => 1024 * 1024,
"G" => 1024 * 1024 * 1024 ];
496 $umf_parts = preg_split(
498 $upload_max_filesize,
502 $pms_parts = preg_split(
509 if (count($umf_parts) === 2) {
510 $upload_max_filesize = $umf_parts[0] * $multiplier_a[$umf_parts[1]];
513 if (count($pms_parts) === 2) {
514 $post_max_size = $pms_parts[0] * $multiplier_a[$pms_parts[1]];
518 $max_filesize = min($upload_max_filesize, $post_max_size);
520 if (!$max_filesize) {
521 $max_filesize = max($upload_max_filesize, $post_max_size);
522 return $max_filesize;
525 return $max_filesize;
531 bool $authorized =
true
533 if ($pass === null || $pass < 0) {
537 $test_id = $this->participant_repository->lookupTestIdByActiveId($active_id);
540 $upload_handling_required = $this->current_cmd !==
'submitSolution'
541 && $this->current_cmd !==
'showInstantResponse'
542 && !$this->isFileDeletionAction()
543 && $this->isFileUploadAvailable()
544 && $this->checkUpload();
546 $this->tpl->setOnScreenMessage(
'failure',
$e->getMessage(),
true);
552 if ($upload_handling_required) {
554 $upload_results = $this->file_upload->getResults();
555 $upload_result = end($upload_results);
556 $rid = $this->irss->manage()->upload(
566 $rids_to_delete = $this->resolveRIDStoDelete();
568 $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
569 function () use ($upload_handling_required, $active_id, $pass, $authorized, $rid) {
570 if ($authorized ===
false) {
571 $this->forceExistingIntermediateSolution($active_id, $pass,
true);
574 if ($this->isFileDeletionAction()) {
575 if ($this->isFileDeletionSubmitAvailable()) {
576 $delete_files = $this->questionpool_request->intArray(self::DELETE_FILES_TBL_POSTVAR);
578 foreach ($delete_files as $solution_id) {
579 $this->removeSolutionRecordById($solution_id);
582 $this->tpl->setOnScreenMessage(
'info', $this->
lng->txt(
'no_checkbox'),
true);
585 if ($this->isFileReuseHandlingRequired()) {
586 $reuse_files = $this->questionpool_request->intArray(self::REUSE_FILES_TBL_POSTVAR);
588 foreach ($reuse_files as $solutionId) {
589 $solution = $this->getSolutionRecordById($solutionId);
591 $this->saveCurrentSolution(
602 if ($upload_handling_required && $rid !==
null) {
603 $revision = $this->irss->manage()->getCurrentRevision($rid);
604 $this->saveCurrentSolution(
615 if ($authorized ===
true && $this->intermediateSolutionExists($active_id, $pass)) {
617 $this->deleteDummySolutionRecord($active_id, $pass);
620 $this->removeCurrentSolution($active_id, $pass,
true);
621 $this->updateCurrentSolutionsAuthorization($active_id, $pass,
true,
true);
627 $this->deleteUnusedFiles(
639 $rids_to_delete = [];
640 if ($this->isFileDeletionAction() && $this->isFileDeletionSubmitAvailable()) {
641 $delete_files = $this->questionpool_request->intArray(self::DELETE_FILES_TBL_POSTVAR);
643 $res = $this->db->query(
644 "SELECT value1 FROM tst_solutions WHERE value2 = 'rid' AND " . $this->db->in(
651 while (
$d = $this->db->fetchAssoc(
$res)) {
652 $rids_to_delete[] =
$d[
'value1'];
655 return $rids_to_delete;
660 return parent::removeSolutionRecordById($solution_id);
667 $solution = $this->getSolutionValues($active_id, $pass, false);
669 if (!count($solution)) {
670 $solution = $this->getSolutionValues($active_id, $pass,
true);
673 foreach ($solution as $row) {
674 if (!empty($row[
'value1'])) {
678 $solution = $cleaned;
686 parent::removeIntermediateSolution($active_id, $pass);
688 $test_id = $this->participant_repository->lookupTestIdByActiveId($active_id);
689 if ($test_id !== -1) {
691 $this->deleteUnusedFiles([], $test_id, $active_id, $pass);
700 if (!is_array($userSolution)) {
705 if ($this->isFileDeletionAction()) {
708 if ($this->isFileDeletionSubmitAvailable()) {
710 $delete_files = $this->questionpool_request->strArray(self::DELETE_FILES_TBL_POSTVAR);
712 $userSolution = $this->deletePreviewFileUploads($previewSession->
getUserId(), $userSolution, $delete_files);
714 $this->tpl->setOnScreenMessage(
'info', $this->
lng->txt(
'no_checkbox'),
true);
719 $fileUploadAvailable = $this->current_cmd !==
'instantResponse'
720 && $this->isFileUploadAvailable();
722 $this->tpl->setOnScreenMessage(
'failure',
$e->getMessage(),
true);
725 if ($fileUploadAvailable) {
727 if ($this->checkUpload()) {
728 if (!@file_exists($this->getPreviewFileUploadPath($previewSession->
getUserId()))) {
733 $filename_arr = pathinfo($_FILES[
'upload'][
'name']);
734 $extension = $filename_arr[
'extension'];
735 $newfile =
'file_' . md5($_FILES[
'upload'][
'name']) .
'_' .
$version .
'.' . $extension;
737 $_FILES[
'upload'][
'tmp_name'],
738 $_FILES[
'upload'][
'name'],
739 $this->getPreviewFileUploadPath($previewSession->
getUserId()) . $newfile
742 $userSolution[$newfile] = [
743 'solution_id' => $newfile,
744 'value1' => $newfile,
745 'value2' => $_FILES[
'upload'][
'name'],
747 'webpath' => $this->getPreviewFileUploadPathWeb($previewSession->
getUserId())
758 return 'assFileUpload';
763 return 'qpl_qst_fileupload';
784 return parent::getRTETextWithMediaObjects();
790 return $user_solution;
795 return $this->maxsize;
800 $this->maxsize = $value;
805 if ($this->allowedextensions ===
'') {
809 return array_filter(array_map(
'trim', explode(
',', $this->allowedextensions)));
814 return $this->allowedextensions;
819 $this->allowedextensions = strtolower(trim($a_value));
825 SELECT tst_solutions.solution_id
826 FROM tst_solutions, tst_active, qpl_questions
827 WHERE tst_solutions.active_fi = tst_active.active_id
828 AND tst_solutions.question_fi = qpl_questions.question_id
829 AND tst_solutions.question_fi = %s AND tst_active.test_fi = %s
830 AND tst_solutions.value1 is not null';
831 $result = $this->db->queryF(
833 [
'integer',
'integer'],
834 [$this->
getId(), $test_id]
836 if ($result->numRows() > 0) {
852 $exporter->setTestTitle($test_title);
853 $exporter->setQuestion($this);
855 $exporter->buildAndDownload();
860 return $this->completion_by_submission;
865 $this->completion_by_submission = (bool) $bool;
871 return parent::buildTestPresentationConfig()
882 return $this->isNonEmptyItemListPostSubmission(self::DELETE_FILES_TBL_POSTVAR);
887 return $this->isNonEmptyItemListPostSubmission(self::REUSE_FILES_TBL_POSTVAR);
892 if (!$this->getTestPresentationConfig()->isPreviousPassSolutionReuseAllowed()) {
896 if (!$this->isFileReuseSubmitAvailable()) {
908 if (!$this->file_upload->hasBeenProcessed()) {
909 $this->file_upload->process();
911 return $this->file_upload->hasUploads();
917 AdditionalInformationGenerator::KEY_QUESTION_TYPE => (string) $this->getQuestionType(),
918 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $this->getTitleForHTMLOutput(),
919 AdditionalInformationGenerator::KEY_QUESTION_TEXT => $this->formatSAQuestion($this->getQuestion()),
920 AdditionalInformationGenerator::KEY_QUESTION_REACHABLE_POINTS => $this->getPoints(),
921 AdditionalInformationGenerator::KEY_QUESTION_UPLOAD_MAXSIZE => $this->getMaxFilesizeAsString(),
922 AdditionalInformationGenerator::KEY_QUESTION_UPLOAD_ALLOWED_EXTENSIONS => $this->getAllowedExtensionsArray(),
923 AdditionalInformationGenerator::KEY_QUESTION_UPLOAD_COMPLETION_BY_SUBMISSION => $additional_info->
getEnabledDisabledTagForBool($this->isCompletionBySubmissionEnabled()),
924 AdditionalInformationGenerator::KEY_FEEDBACK => [
925 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_INCOMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(),
false)),
926 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_COMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(),
true))
933 array $solution_values
936 static fn(array $v): string =>
"{$v['value1']} - {$v['value2']}",
944 static fn(array $v):
string =>
"{$v['value1']} - {$v['value2']}",
Class IllegalStateException.
const DELETE_FILES_ACTION
Class for file upload questions.
ParticipantRepository $participant_repository
isCompletionBySubmissionEnabled()
toLog(AdditionalInformationGenerator $additional_info)
MUST return an array of the question settings that can be stored in the log.
deleteAnswers($question_id)
string $allowedextensions
setCompletionBySubmission(bool $bool)
setAllowedExtensions(string $a_value)
solutionValuesToLog(AdditionalInformationGenerator $additional_info, array $solution_values)
MUST convert the given solution values into an array or a string that can be stored in the log.
ILIAS FileDelivery Services $file_delivery
ILIAS ResourceStorage Services $irss
calculateReachedPoints(int $active_id, ?int $pass=null, bool $authorized_solution=true)
$completion_by_submission
saveToDb(?int $original_id=null)
Saves a assFileUpload object to a database.
getFileUploadPathWeb($test_id, $active_id, $question_id=null)
Returns the file upload path for web accessible files of a question.
getUploadedFiles(int $active_id, ?int $pass=null, bool $authorized=true)
deletePreviewFileUploads($userId, $userSolution, $files)
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
solutionValuesToText(array $solution_values)
MUST convert the given solution values into text.
saveWorkingData(int $active_id, ?int $pass=null, bool $authorized=true)
deliverFileUploadZIPFile(int $ref_id, int $test_id, string $test_title)
removeSolutionRecordById(int $solution_id)
saveAdditionalQuestionDataToDb()
Saves a record to the question types additional data table.
getCorrectSolutionForTextOutput(int $active_id, int $pass)
removeIntermediateSolution(int $active_id, int $pass)
const REUSE_FILES_TBL_POSTVAR
isFileDeletionSubmitAvailable()
getBestSolution($active_id, $pass)
getPreviewFileUploadPath($userId)
Returns the filesystem path for file uploads.
isComplete()
Returns true, if the question is complete for use.
hasFileUploads(int $test_id)
getPreviewFileUploadPathWeb(int $user_id)
calculateReachedPointsForSolution(?array $user_solution)
isFileReuseSubmitAvailable()
getFileUploadPath($test_id, $active_id, $question_id=null)
Returns the filesystem path for file uploads.
isFileReuseHandlingRequired()
ILIAS FileUpload FileUpload $file_upload
savePreviewData(ilAssQuestionPreviewSession $previewSession)
getPreviewFileUploads(ilAssQuestionPreviewSession $previewSession)
getAllowedExtensionsArray()
getUserSolutionPreferingIntermediate(int $active_id, ?int $pass=null)
loadFromDb(int $question_id)
getUploadedFilesForWeb($active_id, $pass)
Returns the web accessible uploaded files for an active user in a given pass.
checkUpload()
Check file upload.
const DELETE_FILES_TBL_POSTVAR
const HAS_SPECIFIC_FEEDBACK
buildTestPresentationConfig()
setOriginalId(?int $original_id)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
setQuestion(string $question="")
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
setAuthor(string $author="")
setComment(string $comment="")
getSolutionMaxPass(int $active_id)
setNrOfTries(int $a_nr_of_tries)
setLifecycle(ilAssQuestionLifecycle $lifecycle)
setTitle(string $title="")
isDummySolutionRecord(array $solutionRecord)
saveQuestionDataToDb(?int $original_id=null)
static getDraftInstance()
static getInstance($identifier)
getParticipantsSolution()
setParticipantsSolution($participantSolution)
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
static removeTrailingPathSeparators(string $path)
static moveUploadedFile(string $a_file, string $a_name, string $a_target, bool $a_raise_errors=true, string $a_mode="move_uploaded")
move uploaded file
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
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...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setFormChangeDetectionEnabled($enableFormChangeDetection)
Set if the detection of form changes is enabled.
return['delivery_method'=> 'php',]
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
if(!file_exists('../ilias.ini.php'))
PREG_SPLIT_NO_EMPTY PREG_SPLIT_DELIM_CAPTURE