Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions lib/BackgroundJob/DeleteQuestionFoldersJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Forms\BackgroundJob;

use OCA\Forms\Db\FormMapper;
use OCA\Forms\Service\FormsService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use Psr\Log\LoggerInterface;

class DeleteQuestionFoldersJob extends QueuedJob {
public function __construct(
ITimeFactory $time,
private FormMapper $formMapper,
private FormsService $formsService,
private IRootFolder $rootFolder,
private LoggerInterface $logger,
) {
parent::__construct($time);
}

/**
* @param array{formId: int, questionId: int, ownerId: string} $argument
*/
public function run($argument): void {
$formId = $argument['formId'];
$questionId = $argument['questionId'];
$ownerId = $argument['ownerId'];

try {
$form = $this->formMapper->findById($formId);
$this->logger->debug('Deleting question folders for question {questionId} in form {formId}', [
'questionId' => $questionId,
'formId' => $formId,
]);

$userFolder = $this->rootFolder->getUserFolder($ownerId);
$formFolderPath = $this->formsService->getFormUploadedFilesFolderPath($form);

$formFolder = $userFolder->get($formFolderPath);
if (!$formFolder instanceof Folder) {
$this->logger->notice('Form folder not found, nothing to delete', [
'formId' => $formId,
]);
return;
}

$questionFolderPrefix = $questionId . ' - ';
$deletedCount = 0;

// 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();
$deletedCount++;
}
}
}

$this->logger->info('Deleted {count} question folders for question {questionId}', [
'count' => $deletedCount,
'questionId' => $questionId,
'formId' => $formId,
]);
} catch (NotFoundException) {
// Folder doesn't exist, do nothing
$this->logger->notice('Question folder not found, nothing to delete', [
'questionId' => $questionId,
'formId' => $formId,
]);
} catch (\Throwable $e) {
$this->logger->warning('Failed to delete question folders: {error}', [
'error' => $e->getMessage(),
'questionId' => $questionId,
'formId' => $formId,
]);
}
}
}
18 changes: 13 additions & 5 deletions lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace OCA\Forms\Controller;

use OCA\Forms\BackgroundJob\DeleteQuestionFoldersJob;
use OCA\Forms\BackgroundJob\SyncSubmissionsWithLinkedFileJob;
use OCA\Forms\Constants;
use OCA\Forms\Db\Answer;
Expand Down Expand Up @@ -46,6 +47,7 @@
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\IL10N;
Expand Down Expand Up @@ -742,6 +744,14 @@ public function deleteQuestion(int $formId, int $questionId): DataResponse {
$question->setOrder(0);
$this->questionMapper->update($question);

if ($question->getType() === Constants::ANSWER_TYPE_FILE) {
$this->jobList->add(DeleteQuestionFoldersJob::class, [
'formId' => $form->getId(),
'questionId' => $question->getId(),
'ownerId' => $form->getOwnerId(),
]);
}

// Update all question-order > deleted order.
$formQuestions = $this->questionMapper->findByForm($formId);
foreach ($formQuestions as $question) {
Expand Down Expand Up @@ -1589,7 +1599,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);
Expand Down Expand Up @@ -1743,8 +1753,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']);
Comment thread
Koc marked this conversation as resolved.
$file = $folder->newFile($fileName, file_get_contents($uploadedFile['tmp_name']));

Expand Down Expand Up @@ -1819,8 +1828,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];
Comment thread
Koc marked this conversation as resolved.
$name = $folder->getNonExistingName($file->getName());
$file->move($folder->getPath() . '/' . $name);
Expand Down
22 changes: 22 additions & 0 deletions lib/Db/AnswerMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,26 @@

$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();

Check failure on line 74 in lib/Db/AnswerMapper.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

UndefinedInterfaceMethod

lib/Db/AnswerMapper.php:74:20: UndefinedInterfaceMethod: Method OCP\DB\IResult::fetchFirstColumn does not exist (see https://psalm.dev/181)
$result->closeCursor();

return array_map('intval', $rows);
}
}
37 changes: 37 additions & 0 deletions lib/Db/FormMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
use OCP\AppFramework\Db\QBMapper;
use OCP\Comments\ICommentsManager;
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<Form>
Expand All @@ -32,6 +36,8 @@ public function __construct(
private SubmissionMapper $submissionMapper,
private ConfigService $configService,
private ICommentsManager $commentsManager,
private IRootFolder $rootFolder,
private LoggerInterface $logger,
) {
parent::__construct($db, 'forms_v2_forms', Form::class);
}
Expand Down Expand Up @@ -225,6 +231,37 @@ public function deleteForm(Form $form): void {
$this->shareMapper->deleteByForm($formId);
$this->questionMapper->deleteByForm($formId);
$this->commentsManager->deleteCommentsAtObject('forms', (string)$formId);
$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(),
]);
}
}
}
58 changes: 47 additions & 11 deletions lib/Db/SubmissionMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@

namespace OCA\Forms\Db;

use OCA\Forms\Service\FormsService;
use OCP\AppFramework\Db\DoesNotExistException;
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 Psr\Log\LoggerInterface;

/**
* @extends QBMapper<Submission>
Expand All @@ -20,10 +25,16 @@ class SubmissionMapper extends QBMapper {
* SubmissionMapper constructor.
* @param IDBConnection $db
* @param AnswerMapper $answerMapper
* @param IRootFolder $rootFolder
* @param LoggerInterface $logger
* @param FormsService $formsService
*/
public function __construct(
IDBConnection $db,
private AnswerMapper $answerMapper,
private IRootFolder $rootFolder,
private LoggerInterface $logger,
private FormsService $formsService,
) {
parent::__construct($db, 'forms_v2_submissions', Submission::class);
}
Expand Down Expand Up @@ -179,22 +190,17 @@ 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))
);
$this->deleteSubmissionFolder($form, $submissionEntity->getId());

$qb->executeStatement();
$this->answerMapper->deleteBySubmission($submissionEntity->getId());

$this->delete($submissionEntity);
}

/**
Expand All @@ -218,4 +224,34 @@ public function deleteByForm(int $formId): void {

$qb->executeStatement();
}

/**
* Delete the submission folder from the file system
* @param Form $form The form instance
* @param int $submissionId The submission ID
*/
private function deleteSubmissionFolder(Form $form, int $submissionId): void {
try {
$userFolder = $this->rootFolder->getUserFolder($form->getOwnerId());
$formFolderPath = $this->formsService->getFormUploadedFilesFolderPath($form);

$formFolder = $userFolder->get($formFolderPath);
if (!$formFolder instanceof Folder) {
return;
}

$submissionFolder = $formFolder->get((string)$submissionId);
if ($submissionFolder instanceof Folder) {
$submissionFolder->delete();
}
} catch (NotFoundException) {
// Folder doesn't exist, do nothing
} catch (\Throwable $e) {
$this->logger->warning('Failed to delete submission folder: {error}', [
'error' => $e->getMessage(),
'submissionId' => $submissionId,
'formId' => $form->getId(),
]);
}
}
}
Loading