diff --git a/appinfo/info.xml b/appinfo/info.xml index 9051497e77..5d52697025 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -25,7 +25,7 @@ Developed with ❤️ by [LibreCode](https://librecode.coop). Help us transform * [Donate with GitHub Sponsor: ![Donate using GitHub Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/libresign) ]]> - 13.0.0-dev.5 + 13.0.0-dev.6 agpl LibreCode 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 088f413ef0..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()->fetchAssociative(); - 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 818d5e9d4f..4333faca0a 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, ) { } @@ -34,53 +32,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; + } +}