diff --git a/lib/Db/File.php b/lib/Db/File.php index 7f0dcfddf4..feaa9ab80b 100644 --- a/lib/Db/File.php +++ b/lib/Db/File.php @@ -17,14 +17,14 @@ * @method void setId(int $id) * @method int getId() * @method void setNodeId(?int $nodeId) - * @method int getNodeId() - * @method void setSignedNodeId(int $nodeId) + * @method ?int getNodeId() + * @method void setSignedNodeId(?int $nodeId) * @method ?int getSignedNodeId() - * @method void setSignedHash(string $hash) + * @method void setSignedHash(?string $hash) * @method ?string getSignedHash() - * @method void setUserId(string $userId) + * @method void setUserId(?string $userId) * @method ?string getUserId() - * @method void setSignRequestId(int $signRequestId) + * @method void setSignRequestId(?int $signRequestId) * @method ?int getSignRequestId() * @method void setUuid(string $uuid) * @method string getUuid() @@ -50,7 +50,7 @@ * @method ?int getParentFileId() */ class File extends Entity { - protected int $nodeId = 0; + protected ?int $nodeId = null; protected string $uuid = ''; protected ?\DateTime $createdAt = null; protected string $name = ''; diff --git a/lib/Db/FileMapper.php b/lib/Db/FileMapper.php index 4d5024ec6d..a02a628cbb 100644 --- a/lib/Db/FileMapper.php +++ b/lib/Db/FileMapper.php @@ -220,44 +220,6 @@ public function getFilesOfAccount(string $userId): array { return $return; } - public function getDeletionContext(int $nodeId): array { - $fullOuterJoin = $this->db->getQueryBuilder(); - $fullOuterJoin->select($fullOuterJoin->expr()->literal(1)); - - $qb = $this->db->getQueryBuilder(); - $qb - ->selectAlias('f.id', 'file_id') - ->selectAlias('sf.id', 'signed_file_id') - ->selectAlias('ue.id', 'user_element_id') - ->selectAlias('fe.file_id', 'file_element_file_id') - ->from($qb->createFunction('(' . $fullOuterJoin->getSQL() . ')'), 'foj') - ->leftJoin('foj', 'libresign_file', 'f', $qb->expr()->eq('f.node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))) - ->leftJoin('foj', 'libresign_file', 'sf', $qb->expr()->eq('sf.signed_node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))) - ->leftJoin('foj', 'libresign_user_element', 'ue', $qb->expr()->eq('ue.node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))) - ->leftJoin('foj', 'libresign_file_element', 'fe', $qb->expr()->eq('fe.file_id', 'f.id')) - ->setMaxResults(1); - - $row = $qb->executeQuery()->fetch(); - if (!$row) { - return ['type' => 'not_libresign_file', 'fileId' => null]; - } - - if (!empty($row['signed_file_id'])) { - return ['type' => 'signed_file', 'fileId' => (int)$row['signed_file_id']]; - } - if (!empty($row['file_id'])) { - return ['type' => 'file', 'fileId' => (int)$row['file_id']]; - } - if (!empty($row['user_element_id'])) { - return ['type' => 'user_element', 'fileId' => null]; - } - if (!empty($row['file_element_file_id'])) { - return ['type' => 'file_element', 'fileId' => (int)$row['file_element_file_id']]; - } - - return ['type' => 'not_libresign_file', 'fileId' => null]; - } - public function getTextOfStatus(int|FileStatus $status): string { if (is_int($status)) { $status = FileStatus::from($status); diff --git a/lib/Listener/BeforeNodeDeletedListener.php b/lib/Listener/BeforeNodeDeletedListener.php index ab476f501c..5df627c7d7 100644 --- a/lib/Listener/BeforeNodeDeletedListener.php +++ b/lib/Listener/BeforeNodeDeletedListener.php @@ -8,15 +8,15 @@ namespace OCA\Libresign\Listener; -use OCA\Libresign\Db\FileMapper; +use OCA\Libresign\Enum\FileStatus; use OCA\Libresign\Helper\ValidateHelper; -use OCA\Libresign\Service\RequestSignatureService; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\Files\Cache\CacheEntryRemovedEvent; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; use OCP\Files\File; +use OCP\Files\Folder; use OCP\IDBConnection; /** @@ -24,8 +24,6 @@ */ class BeforeNodeDeletedListener implements IEventListener { public function __construct( - private FileMapper $fileMapper, - private RequestSignatureService $requestSignatureService, private IDBConnection $db, ) { } @@ -33,53 +31,177 @@ public function __construct( public function handle(Event $event): void { if ($event instanceof BeforeNodeDeletedEvent) { $node = $event->getNode(); - if (!$node instanceof File) { + if (!$node instanceof File && !$node instanceof Folder) { return; } - if (!in_array($node->getMimeType(), ValidateHelper::VALID_MIMETIPE)) { + if ($node instanceof File && !in_array($node->getMimeType(), ValidateHelper::VALID_MIMETIPE)) { return; } - $nodeId = $node->getId(); - $this->delete($nodeId); + + $this->deleteAllByNodeId($node->getId()); return; } + if ($event instanceof CacheEntryRemovedEvent) { - $this->delete($event->getFileId()); + $this->deleteAllByNodeId($event->getFileId()); } - return; } - private function delete(int $nodeId): void { - $context = $this->fileMapper->getDeletionContext($nodeId); - if ($context['type'] === 'not_libresign_file') { + private function deleteAllByNodeId(int $nodeId): void { + if ($this->handleSignedFileDeleted($nodeId)) { return; } - switch ($context['type']) { - case 'signed_file': - if (!isset($context['fileId'])) { - return; - } - $this->requestSignatureService->deleteRequestSignature(['file' => ['fileId' => $context['fileId']]]); - break; - case 'file': - $libresignFile = $this->fileMapper->getByNodeId($nodeId); - $this->requestSignatureService->deleteRequestSignature(['file' => ['fileId' => $libresignFile->getId()]]); - $this->fileMapper->delete($libresignFile); - break; - case 'user_element': - $qb = $this->db->getQueryBuilder(); - $qb->delete('libresign_user_element') - ->where($qb->expr()->eq('node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))) - ->executeStatement(); - break; - case 'file_element': - if (!isset($context['fileId'])) { - return; + + $fullOuterJoin = $this->db->getQueryBuilder(); + $fullOuterJoin->select($fullOuterJoin->expr()->literal(1)); + + $qb = $this->db->getQueryBuilder(); + $qb + ->selectAlias('current.id', 'file_id') + ->selectAlias('current.metadata', 'file_metadata') + ->selectAlias('current.signed_node_id', 'current_signed_node_id') + ->selectAlias('parent.id', 'parent_id') + ->selectAlias('children.id', 'child_id') + ->selectAlias('ue.id', 'user_element_id') + ->selectAlias('fe.file_id', 'file_element_file_id') + ->from($qb->createFunction('(' . $fullOuterJoin->getSQL() . ')'), 'foj') + ->leftJoin('foj', 'libresign_file', 'current', $qb->expr()->eq('current.node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))) + ->leftJoin('current', 'libresign_file', 'parent', $qb->expr()->eq('parent.id', 'current.parent_file_id')) + ->leftJoin('current', 'libresign_file', 'children', $qb->expr()->eq('children.parent_file_id', 'current.id')) + ->leftJoin('foj', 'libresign_user_element', 'ue', $qb->expr()->eq('ue.node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))) + ->leftJoin('foj', 'libresign_file_element', 'fe', $qb->expr()->eq('fe.file_id', 'current.id')); + + $cursor = $qb->executeQuery(); + + $deletedFiles = []; + $deletedUserElements = false; + $deletedFileElements = []; + + while ($row = $cursor->fetch()) { + if (!empty($row['user_element_id']) && !$deletedUserElements) { + $deletedUserElements = true; + $this->deleteUserElement($nodeId); + } + + if (!empty($row['file_element_file_id']) && !isset($deletedFileElements[$row['file_element_file_id']])) { + $deletedFileElements[(int)$row['file_element_file_id']] = true; + $this->deleteFileElement((int)$row['file_element_file_id']); + } + + if (!empty($row['file_id']) && !isset($deletedFiles[$row['file_id']])) { + $deletedFiles[(int)$row['file_id']] = true; + $this->markOriginalFileAsDeleted((int)$row['file_id'], isset($row['file_metadata']) ? (string)$row['file_metadata'] : null); + } + } + $cursor->closeCursor(); + } + + private function handleSignedFileDeleted(int $nodeId): bool { + $qb = $this->db->getQueryBuilder(); + $qb->select('id', 'node_id', 'signed_node_id') + ->from('libresign_file') + ->where($qb->expr()->eq('signed_node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))); + + $files = $qb->executeQuery()->fetchAll(); + + foreach ($files as $file) { + $fileId = (int)$file['id']; + $this->deleteSigningData($fileId); + $this->detachSignedFile($fileId); + } + + return !empty($files); + } + + private function markOriginalFileAsDeleted(int $fileId, ?string $metadataJson = null): void { + $existingMetadata = []; + + if ($metadataJson !== null && $metadataJson !== '') { + try { + $decoded = json_decode($metadataJson, true, 512, JSON_THROW_ON_ERROR); + if (is_array($decoded)) { + $existingMetadata = $decoded; } - $qb = $this->db->getQueryBuilder(); - $qb->delete('libresign_file_element') - ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($context['fileId'], IQueryBuilder::PARAM_INT))) - ->executeStatement(); + } catch (\Throwable $e) { + } + } + + $existingMetadata['original_file_deleted'] = true; + $existingMetadata['original_file_deleted_at'] = (new \DateTime('now', new \DateTimeZone('UTC')))->format(\DateTime::ATOM); + + $update = $this->db->getQueryBuilder(); + $update->update('libresign_file') + ->set('node_id', $update->createNamedParameter(null, IQueryBuilder::PARAM_NULL)) + ->set('metadata', $update->createNamedParameter(json_encode($existingMetadata), IQueryBuilder::PARAM_STR)) + ->where($update->expr()->eq('id', $update->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->executeStatement(); + } + + private function detachSignedFile(int $fileId): void { + $qb = $this->db->getQueryBuilder(); + $qb->update('libresign_file') + ->set('signed_node_id', $qb->createNamedParameter(null, IQueryBuilder::PARAM_NULL)) + ->set('status', $qb->createNamedParameter(FileStatus::DRAFT->value, IQueryBuilder::PARAM_INT)) + ->set('signed_hash', $qb->createNamedParameter(null, IQueryBuilder::PARAM_NULL)) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->executeStatement(); + } + + private function deleteUserElement(int $nodeId): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete('libresign_user_element') + ->where($qb->expr()->eq('node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))) + ->executeStatement(); + } + + private function deleteFileElement(int $fileId): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete('libresign_file_element') + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->executeStatement(); + } + + private function deleteSigningData(int $fileId): void { + $this->deleteIdentifyMethods($fileId); + $this->deleteSignRequests($fileId); + $this->deleteIdDocs($fileId); + $this->deleteFileElement($fileId); + } + + private function deleteIdentifyMethods(int $fileId): void { + $qb = $this->db->getQueryBuilder(); + $qb->select('id') + ->from('libresign_sign_request') + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + $cursor = $qb->executeQuery(); + + while ($row = $cursor->fetch()) { + $delete = $this->db->getQueryBuilder(); + $delete->delete('libresign_identify_method') + ->where($delete->expr()->eq('sign_request_id', $delete->createNamedParameter($row['id'], IQueryBuilder::PARAM_INT))) + ->executeStatement(); } + $cursor->closeCursor(); + } + + private function deleteSignRequests(int $fileId): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete('libresign_sign_request') + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->executeStatement(); + } + + private function deleteIdDocs(int $fileId): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete('libresign_id_docs') + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->executeStatement(); + } + + private function deleteFile(int $fileId): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete('libresign_file') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->executeStatement(); } } diff --git a/lib/Migration/Version17000Date20260106000000.php b/lib/Migration/Version17000Date20260106000000.php new file mode 100644 index 0000000000..e5314c507a --- /dev/null +++ b/lib/Migration/Version17000Date20260106000000.php @@ -0,0 +1,36 @@ +hasTable('libresign_file')) { + $table = $schema->getTable('libresign_file'); + + if ($table->hasColumn('node_id')) { + $table->modifyColumn('node_id', [ + 'notnull' => false, + ]); + $changed = true; + } + } + + return $changed ? $schema : null; + } +} diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 7a184afa9e..3d7f69d8c1 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -240,9 +240,9 @@ * visibleElements?: LibresignVisibleElement[], * } * @psalm-type LibresignFileListItem = array{ - * nodeId: int, + * nodeId: ?int, * uuid: string, - * name: string, + * name: non-falsy-string, * status: int, * statusText: string, * } diff --git a/lib/Service/SignFileService.php b/lib/Service/SignFileService.php index d86ca88994..f7348ee34d 100644 --- a/lib/Service/SignFileService.php +++ b/lib/Service/SignFileService.php @@ -1024,7 +1024,14 @@ public function getNextcloudFiles(FileEntity $fileData): array { $children = $this->fileMapper->getChildrenFiles($fileData->getId()); $files = []; foreach ($children as $child) { - $file = $this->root->getUserFolder($child->getUserId())->getFirstNodeById($child->getNodeId()); + $nodeId = $child->getNodeId(); + if ($nodeId === null) { + throw new LibresignException(json_encode([ + 'action' => JSActions::ACTION_DO_NOTHING, + 'errors' => [['message' => $this->l10n->t('File not found')]], + ]), AppFrameworkHttp::STATUS_NOT_FOUND); + } + $file = $this->root->getUserFolder($child->getUserId())->getFirstNodeById($nodeId); if ($file instanceof File) { $files[] = $file; } @@ -1032,7 +1039,14 @@ public function getNextcloudFiles(FileEntity $fileData): array { return $files; } - $fileToSign = $this->root->getUserFolder($fileData->getUserId())->getFirstNodeById($fileData->getNodeId()); + $nodeId = $fileData->getNodeId(); + if ($nodeId === null) { + throw new LibresignException(json_encode([ + 'action' => JSActions::ACTION_DO_NOTHING, + 'errors' => [['message' => $this->l10n->t('File not found')]], + ]), AppFrameworkHttp::STATUS_NOT_FOUND); + } + $fileToSign = $this->root->getUserFolder($fileData->getUserId())->getFirstNodeById($nodeId); if (!$fileToSign instanceof File) { throw new LibresignException(json_encode([ 'action' => JSActions::ACTION_DO_NOTHING, @@ -1050,7 +1064,14 @@ public function getNextcloudFilesWithEntities(FileEntity $fileData): array { $children = $this->fileMapper->getChildrenFiles($fileData->getId()); $result = []; foreach ($children as $child) { - $file = $this->root->getUserFolder($child->getUserId())->getFirstNodeById($child->getNodeId()); + $nodeId = $child->getNodeId(); + if ($nodeId === null) { + throw new LibresignException(json_encode([ + 'action' => JSActions::ACTION_DO_NOTHING, + 'errors' => [['message' => $this->l10n->t('File not found')]], + ]), AppFrameworkHttp::STATUS_NOT_FOUND); + } + $file = $this->root->getUserFolder($child->getUserId())->getFirstNodeById($nodeId); if ($file instanceof File) { $result[] = $child; } @@ -1058,7 +1079,14 @@ public function getNextcloudFilesWithEntities(FileEntity $fileData): array { return $result; } - $fileToSign = $this->root->getUserFolder($fileData->getUserId())->getFirstNodeById($fileData->getNodeId()); + $nodeId = $fileData->getNodeId(); + if ($nodeId === null) { + throw new LibresignException(json_encode([ + 'action' => JSActions::ACTION_DO_NOTHING, + 'errors' => [['message' => $this->l10n->t('File not found')]], + ]), AppFrameworkHttp::STATUS_NOT_FOUND); + } + $fileToSign = $this->root->getUserFolder($fileData->getUserId())->getFirstNodeById($nodeId); if (!$fileToSign instanceof File) { throw new LibresignException(json_encode([ 'action' => JSActions::ACTION_DO_NOTHING, diff --git a/openapi-full.json b/openapi-full.json index 9b1c4c090a..7e7453ff8a 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -617,7 +617,8 @@ "properties": { "nodeId": { "type": "integer", - "format": "int64" + "format": "int64", + "nullable": true }, "uuid": { "type": "string" diff --git a/openapi.json b/openapi.json index 19db86cc34..1cc8c82265 100644 --- a/openapi.json +++ b/openapi.json @@ -547,7 +547,8 @@ "properties": { "nodeId": { "type": "integer", - "format": "int64" + "format": "int64", + "nullable": true }, "uuid": { "type": "string" diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index f97021e92a..dcd37fbc92 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -1631,7 +1631,7 @@ export type components = { }; FileListItem: { /** Format: int64 */ - nodeId: number; + nodeId: number | null; uuid: string; name: string; /** Format: int64 */ diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 56e1052683..5f4b5202a0 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1175,7 +1175,7 @@ export type components = { }; FileListItem: { /** Format: int64 */ - nodeId: number; + nodeId: number | null; uuid: string; name: string; /** Format: int64 */