diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index e98d5c188..1285f0f77 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -46,8 +46,10 @@ use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCSController; use OCP\BackgroundJob\IJobList; +use OCP\Files\Folder; use OCP\Files\IMimeTypeDetector; use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; use OCP\IL10N; use OCP\IRequest; use OCP\IUser; @@ -742,6 +744,10 @@ public function deleteQuestion(int $formId, int $questionId): DataResponse { $question->setOrder(0); $this->questionMapper->update($question); + if ($question->getType() === Constants::ANSWER_TYPE_FILE) { + $this->deleteQuestionFolder($form, $question); + } + // Update all question-order > deleted order. $formQuestions = $this->questionMapper->findByForm($formId); foreach ($formQuestions as $question) { @@ -761,6 +767,43 @@ public function deleteQuestion(int $formId, int $questionId): DataResponse { return new DataResponse($questionId); } + /** + * Delete question folders from all submissions + * @param Form $form The form + * @param Question $question The question + */ + private function deleteQuestionFolder(Form $form, Question $question): void { + try { + $userFolder = $this->rootFolder->getUserFolder($form->getOwnerId()); + $formFolderPath = $this->formsService->getFormUploadedFilesFolderPath($form); + + $formFolder = $userFolder->get($formFolderPath); + if (!$formFolder instanceof Folder) { + return; + } + $questionFolderPrefix = $question->getId() . ' - '; + + // Iterate through submission folders and delete matching question folders + foreach ($formFolder->getDirectoryListing() as $submissionFolder) { + if (!$submissionFolder instanceof Folder) { + continue; + } + foreach ($submissionFolder->getDirectoryListing() as $node) { + if (str_starts_with($node->getName(), $questionFolderPrefix)) { + $node->delete(); + } + } + } + } catch (NotFoundException) { + // Do nothing + } catch (\Throwable $e) { + $this->logger->warning('Failed to delete question folders: {error}', [ + 'error' => $e->getMessage(), + 'questionId' => $question->getId(), + ]); + } + } + /** * Updates the Order of all Questions of a Form * @@ -1589,7 +1632,7 @@ public function deleteSubmission(int $formId, int $submissionId): DataResponse { } // Delete submission (incl. Answers) - $this->submissionMapper->deleteById($submissionId); + $this->submissionMapper->deleteById($form, $submissionId); $this->formMapper->update($form); return new DataResponse($submissionId); @@ -1743,7 +1786,7 @@ public function uploadFiles(int $formId, int $questionId, string $shareHash = '' } else { $folder = $userFolder->newFolder($path); } - /** @var \OCP\Files\Folder $folder */ + /** @var Folder $folder */ $fileName = $folder->getNonExistingName($uploadedFile['name']); $file = $folder->newFile($fileName, file_get_contents($uploadedFile['tmp_name'])); @@ -1819,7 +1862,7 @@ private function storeAnswersForQuestion(Form $form, $submissionId, array $quest } else { $folder = $userFolder->newFolder($path); } - /** @var \OCP\Files\Folder $folder */ + /** @var Folder $folder */ $file = $userFolder->getById($uploadedFile->getFileId())[0]; $name = $folder->getNonExistingName($file->getName()); diff --git a/lib/Db/AnswerMapper.php b/lib/Db/AnswerMapper.php index 2420f6b34..600b127db 100644 --- a/lib/Db/AnswerMapper.php +++ b/lib/Db/AnswerMapper.php @@ -54,4 +54,48 @@ public function deleteBySubmission(int $submissionId): void { $qb->executeStatement(); } + + /** + * Collect all fileIds for answers of a specific submission + * @param int $submissionId + * @return int[] Array of fileIds + */ + public function findFileIdsBySubmission(int $submissionId): array { + $qb = $this->db->getQueryBuilder(); + + $qb->select('file_id') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('submission_id', $qb->createNamedParameter($submissionId, IQueryBuilder::PARAM_INT)) + ) + ->andWhere($qb->expr()->isNotNull('file_id')); + + $result = $qb->executeQuery(); + $rows = $result->fetchFirstColumn(); + $result->closeCursor(); + + return array_map('intval', $rows); + } + + /** + * Collect all fileIds for answers of a specific question + * @param int $questionId + * @return int[] Array of fileIds + */ + public function findFileIdsByQuestion(int $questionId): array { + $qb = $this->db->getQueryBuilder(); + + $qb->select('file_id') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('question_id', $qb->createNamedParameter($questionId, IQueryBuilder::PARAM_INT)) + ) + ->andWhere($qb->expr()->isNotNull('file_id')); + + $result = $qb->executeQuery(); + $rows = $result->fetchFirstColumn(); + $result->closeCursor(); + + return array_map('intval', $rows); + } } diff --git a/lib/Db/FormMapper.php b/lib/Db/FormMapper.php index 9187b9835..10a9d9cf0 100644 --- a/lib/Db/FormMapper.php +++ b/lib/Db/FormMapper.php @@ -12,8 +12,12 @@ use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; use OCP\IDBConnection; use OCP\Share\IShare; +use Psr\Log\LoggerInterface; /** * @extends QBMapper
@@ -30,6 +34,8 @@ public function __construct( private ShareMapper $shareMapper, private SubmissionMapper $submissionMapper, private ConfigService $configService, + private IRootFolder $rootFolder, + private LoggerInterface $logger, ) { parent::__construct($db, 'forms_v2_forms', Form::class); } @@ -221,6 +227,39 @@ public function deleteForm(Form $form): void { $this->submissionMapper->deleteByForm($form->getId()); $this->shareMapper->deleteByForm($form->getId()); $this->questionMapper->deleteByForm($form->getId()); + + $this->deleteFormFolder($form); + $this->delete($form); } + + /** + * Delete the form folder from the file system + * @param Form $form The form instance + */ + private function deleteFormFolder(Form $form): void { + try { + $userFolder = $this->rootFolder->getUserFolder($form->getOwnerId()); + $formsFolder = $userFolder->get(Constants::FILES_FOLDER); + + if (!$formsFolder instanceof Folder) { + return; + } + $formFolderPrefix = $form->getId() . ' - '; + + // Iterate through form folders and delete matching folders + foreach ($formsFolder->getDirectoryListing() as $node) { + if (str_starts_with($node->getName(), $formFolderPrefix)) { + $node->delete(); + } + } + } catch (NotFoundException) { + // do nothing + } catch (\Throwable $e) { + $this->logger->warning('Failed to delete form folder: {error}', [ + 'error' => $e->getMessage(), + 'formId' => $form->getId(), + ]); + } + } } diff --git a/lib/Db/SubmissionMapper.php b/lib/Db/SubmissionMapper.php index 367d94504..e4b3f52ae 100644 --- a/lib/Db/SubmissionMapper.php +++ b/lib/Db/SubmissionMapper.php @@ -10,7 +10,9 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\IRootFolder; use OCP\IDBConnection; +use Psr\Log\LoggerInterface; /** * @extends QBMapper @@ -20,10 +22,14 @@ class SubmissionMapper extends QBMapper { * SubmissionMapper constructor. * @param IDBConnection $db * @param AnswerMapper $answerMapper + * @param IRootFolder $rootFolder + * @param LoggerInterface $logger */ public function __construct( IDBConnection $db, private AnswerMapper $answerMapper, + private IRootFolder $rootFolder, + private LoggerInterface $logger, ) { parent::__construct($db, 'forms_v2_submissions', Submission::class); } @@ -179,22 +185,20 @@ protected function countSubmissionsWithFilters(int $formId, ?string $userId = nu /** * Delete the Submission, including answers. + * @param Form $form Form the submission belongs to. * @param int $id of the submission to delete */ - public function deleteById(int $id): void { - $qb = $this->db->getQueryBuilder(); - - // First delete corresponding answers. + public function deleteById(Form $form, int $id): void { $submissionEntity = $this->findById($id); - $this->answerMapper->deleteBySubmission($submissionEntity->getId()); - //Delete Submission - $qb->delete($this->getTableName()) - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) - ); + $fileIds = $this->answerMapper->findFileIdsBySubmission($submissionEntity->getId()); + if (!empty($fileIds)) { + $this->deleteFilesByIds($fileIds, $form->getOwnerId()); + } - $qb->executeStatement(); + $this->answerMapper->deleteBySubmission($submissionEntity->getId()); + + $this->delete($submissionEntity); } /** @@ -218,4 +222,30 @@ public function deleteByForm(int $formId): void { $qb->executeStatement(); } + + /** + * Delete files by their IDs + * @param int[] $fileIds Array of file IDs to delete + * @param string $userId User ID to get the user folder + */ + private function deleteFilesByIds(array $fileIds, string $userId): void { + if (empty($fileIds)) { + return; + } + + try { + $userFolder = $this->rootFolder->getUserFolder($userId); + foreach ($fileIds as $fileId) { + $nodes = $userFolder->getById($fileId); + if (!empty($nodes)) { + $nodes[0]->delete(); + } + } + } catch (\Throwable $e) { + $this->logger->warning('Failed to delete files: {error}', [ + 'error' => $e->getMessage(), + 'fileIds' => $fileIds, + ]); + } + } }