diff --git a/lib/Db/FileMapper.php b/lib/Db/FileMapper.php index dd41b3b532..4d5024ec6d 100644 --- a/lib/Db/FileMapper.php +++ b/lib/Db/FileMapper.php @@ -220,31 +220,42 @@ public function getFilesOfAccount(string $userId): array { return $return; } - public function getFileType(int $id): string { + 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') - ->selectAlias('sf.signed_node_id', 'signed_file') - ->selectAlias('ue.id', 'user_element') - ->selectAlias('fe.id', 'file_element') + ->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($id, IQueryBuilder::PARAM_INT))) - ->leftJoin('foj', 'libresign_file', 'sf', $qb->expr()->eq('sf.signed_node_id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) - ->leftJoin('foj', 'libresign_user_element', 'ue', $qb->expr()->eq('ue.file_id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) - ->leftJoin('foj', 'libresign_file_element', 'fe', $qb->expr()->eq('fe.file_id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); - $cursor = $qb->executeQuery(); - $row = $cursor->fetch(); - if ($row) { - foreach ($row as $key => $value) { - if ($value) { - return $key; - } - } + ->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]; } - return 'not_libresign_file'; + + 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 { diff --git a/lib/Db/UserElement.php b/lib/Db/UserElement.php index dddaec1541..d8a2fe176e 100644 --- a/lib/Db/UserElement.php +++ b/lib/Db/UserElement.php @@ -16,8 +16,8 @@ * @method int getId() * @method void setType(string $type) * @method string getType() - * @method void setFileId(int $fileId) - * @method int getFileId() + * @method void setNodeId(int $nodeId) + * @method int getNodeId() * @method void setUserId(string $userId) * @method void setStarred(int $starred) * @method int getStarred() @@ -27,7 +27,7 @@ */ class UserElement extends Entity { public string $type = ''; - protected int $fileId = 0; + protected int $nodeId = 0; protected string $userId = ''; public bool $starred = false; public ?\DateTime $createdAt = null; @@ -37,7 +37,7 @@ class UserElement extends Entity { public function __construct() { $this->addType('id', Types::INTEGER); $this->addType('type', Types::STRING); - $this->addType('fileId', Types::INTEGER); + $this->addType('nodeId', Types::INTEGER); $this->addType('userId', Types::STRING); $this->addType('starred', Types::INTEGER); $this->addType('createdAt', Types::DATETIME); diff --git a/lib/Db/UserElementMapper.php b/lib/Db/UserElementMapper.php index c257fc61da..9b4e2894a7 100644 --- a/lib/Db/UserElementMapper.php +++ b/lib/Db/UserElementMapper.php @@ -34,9 +34,9 @@ private function getQueryBuilder(array $data): IQueryBuilder { $qb->expr()->eq('ue.id', $qb->createNamedParameter($data['id'], IQueryBuilder::PARAM_INT)) ); } - if (isset($data['file_id'])) { + if (isset($data['node_id'])) { $qb->andWhere( - $qb->expr()->eq('ue.file_id', $qb->createNamedParameter($data['file_id'], IQueryBuilder::PARAM_INT)) + $qb->expr()->eq('ue.node_id', $qb->createNamedParameter($data['node_id'], IQueryBuilder::PARAM_INT)) ); } if (isset($data['type'])) { diff --git a/lib/Helper/ValidateHelper.php b/lib/Helper/ValidateHelper.php index 2ff3c34293..ac4b1d55e8 100644 --- a/lib/Helper/ValidateHelper.php +++ b/lib/Helper/ValidateHelper.php @@ -302,7 +302,7 @@ public function validateVisibleElementsRelation(array $list, SignRequest $signRe $this->validateSignerIsOwnerOfPdfVisibleElement($elements['documentElementId'], $signRequest); if ($canCreateSignature && $user instanceof IUser) { try { - $this->userElementMapper->findOne(['file_id' => $elements['profileNodeId'], 'user_id' => $user->getUID()]); + $this->userElementMapper->findOne(['node_id' => $elements['profileNodeId'], 'user_id' => $user->getUID()]); } catch (\Throwable) { throw new LibresignException($this->l10n->t('Field %s does not belong to user', $elements['profileNodeId'])); } diff --git a/lib/Listener/BeforeNodeDeletedListener.php b/lib/Listener/BeforeNodeDeletedListener.php index c0769cf65d..ab476f501c 100644 --- a/lib/Listener/BeforeNodeDeletedListener.php +++ b/lib/Listener/BeforeNodeDeletedListener.php @@ -50,14 +50,16 @@ public function handle(Event $event): void { } private function delete(int $nodeId): void { - $type = $this->fileMapper->getFileType($nodeId); - if ($type === 'not_libresign_file') { + $context = $this->fileMapper->getDeletionContext($nodeId); + if ($context['type'] === 'not_libresign_file') { return; } - switch ($type) { + switch ($context['type']) { case 'signed_file': - $file = $this->fileMapper->getByNodeId($nodeId); - $this->requestSignatureService->deleteRequestSignature(['file' => ['fileId' => $file->getId()]]); + if (!isset($context['fileId'])) { + return; + } + $this->requestSignatureService->deleteRequestSignature(['file' => ['fileId' => $context['fileId']]]); break; case 'file': $libresignFile = $this->fileMapper->getByNodeId($nodeId); @@ -65,11 +67,18 @@ private function delete(int $nodeId): void { $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': - $field = $type === 'file' ? 'node_id' : 'file_id'; + if (!isset($context['fileId'])) { + return; + } $qb = $this->db->getQueryBuilder(); - $qb->delete('libresign_' . $type) - ->where($qb->expr()->eq($field, $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))) + $qb->delete('libresign_file_element') + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($context['fileId'], IQueryBuilder::PARAM_INT))) ->executeStatement(); } } diff --git a/lib/Migration/Version16002Date20251230120000.php b/lib/Migration/Version16002Date20251230120000.php new file mode 100644 index 0000000000..004c63b5f4 --- /dev/null +++ b/lib/Migration/Version16002Date20251230120000.php @@ -0,0 +1,140 @@ +appData = $appDataFactory->get('libresign'); + } + + #[\Override] + public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $this->backupUserElementTable(); + } + + #[\Override] + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + $changed = false; + + if ($schema->hasTable('libresign_user_element')) { + $table = $schema->getTable('libresign_user_element'); + if (!$table->hasColumn('node_id')) { + $table->addColumn('node_id', Types::BIGINT, [ + 'notnull' => false, + 'unsigned' => true, + ]); + $changed = true; + } + + if ($table->hasColumn('file_id')) { + $table->dropColumn('file_id'); + $changed = true; + } + } + + return $changed ? $schema : null; + } + + private function backupUserElementTable(): void { + $qb = $this->connection->getQueryBuilder(); + $qb->select('*') + ->from('libresign_user_element') + ->orderBy('id'); + + $cursor = $qb->executeQuery(); + $row = $cursor->fetch(); + if (!$row) { + $cursor->closeCursor(); + return; + } + + $folder = $this->appData->getFolder('/'); + $file = $folder->newFile('backup-table-libresign_user_element_Version16002Date20251230120000.csv'); + $fp = $file->write(); + + fputcsv($fp, array_keys($row)); + fputcsv($fp, $row); + while ($row = $cursor->fetch()) { + fputcsv($fp, $row); + } + + fclose($fp); + $cursor->closeCursor(); + } + + private function restoreNodeIdsFromBackup(): void { + $folder = $this->appData->getFolder('/'); + $filename = 'backup-table-libresign_user_element_Version16002Date20251230120000.csv'; + if (!$folder->fileExists($filename)) { + return; + } + + $file = $folder->getFile($filename); + $handle = $file->read(); + if ($handle === false) { + return; + } + + $header = fgetcsv($handle); + if ($header === false) { + fclose($handle); + return; + } + + $columnIndex = array_flip($header); + if (!isset($columnIndex['id']) || !isset($columnIndex['file_id'])) { + fclose($handle); + return; + } + + while (($row = fgetcsv($handle)) !== false) { + if (!isset($row[$columnIndex['id']])) { + continue; + } + + $userElementId = (int)$row[$columnIndex['id']]; + $nodeId = $row[$columnIndex['file_id']] ?? null; + if ($nodeId === null || $nodeId === '') { + continue; + } + + $qb = $this->connection->getQueryBuilder(); + $qb->update('libresign_user_element') + ->set('node_id', $qb->createNamedParameter((int)$nodeId, IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($userElementId, IQueryBuilder::PARAM_INT))); + + $qb->executeStatement(); + } + + fclose($handle); + } + + #[\Override] + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $this->restoreNodeIdsFromBackup(); + } + +} diff --git a/lib/Service/AccountService.php b/lib/Service/AccountService.php index 7a0ae00114..4044fe50f4 100644 --- a/lib/Service/AccountService.php +++ b/lib/Service/AccountService.php @@ -336,7 +336,7 @@ public function getPdfByUuid(string $uuid): File { public function getFileByNodeId(int $nodeId): File { try { - return $this->folderService->getFileById($nodeId); + return $this->folderService->getFileByNodeId($nodeId); } catch (NotFoundException) { throw new DoesNotExistException('Not found'); } @@ -394,7 +394,7 @@ private function updateFileOfVisibleElement(array $data): void { return; } $userElement = $this->userElementMapper->findOne(['id' => $data['elementId']]); - $file = $this->folderService->getFileById($userElement->getFileId()); + $file = $this->folderService->getFileByNodeId($userElement->getNodeId()); $file->putContent($this->getFileRaw($data)); } @@ -449,7 +449,7 @@ private function createFileOfVisibleElementUsingSession(array $data, string $ses private function insertVisibleElement(array $data, IUser $user, File $file): void { $userElement = new UserElement(); $userElement->setType($data['type']); - $userElement->setFileId($file->getId()); + $userElement->setNodeId($file->getId()); $userElement->setUserId($user->getUID()); $userElement->setStarred(isset($data['starred']) && $data['starred'] ? 1 : 0); $userElement->setCreatedAt($this->timeFactory->getDateTime()); @@ -489,12 +489,12 @@ private function getFileRaw(array $data): string { public function deleteSignatureElement(?IUser $user, string $sessionId, int $nodeId): void { if ($user instanceof IUser) { $element = $this->userElementMapper->findOne([ - 'file_id' => $nodeId, + 'node_id' => $nodeId, 'user_id' => $user->getUID(), ]); $this->userElementMapper->delete($element); try { - $file = $this->folderService->getFileById($element->getFileId()); + $file = $this->folderService->getFileByNodeId($element->getNodeId()); $file->delete(); } catch (NotFoundException) { } diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index e72fcc99a3..aec638fbeb 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -109,7 +109,7 @@ public function getNodeFromData(array $data): Node { return $data['file']['fileNode']; } if (isset($data['file']['fileId'])) { - return $this->folderService->getFileById($data['file']['fileId']); + return $this->folderService->getFileByNodeId($data['file']['fileId']); } if (isset($data['file']['path'])) { return $this->folderService->getFileByPath($data['file']['path']); @@ -589,11 +589,11 @@ public function delete(int $fileId): void { $this->idDocsMapper->deleteByFileId($file->getId()); $this->fileMapper->delete($file); if ($file->getSignedNodeId()) { - $signedNextcloudFile = $this->folderService->getFileById($file->getSignedNodeId()); + $signedNextcloudFile = $this->folderService->getFileByNodeId($file->getSignedNodeId()); $signedNextcloudFile->delete(); } try { - $nextcloudFile = $this->folderService->getFileById($fileId); + $nextcloudFile = $this->folderService->getFileByNodeId($file->getNodeId()); $nextcloudFile->delete(); } catch (NotFoundException) { } diff --git a/lib/Service/FolderService.php b/lib/Service/FolderService.php index 6d55b522f9..50f4c8e78b 100644 --- a/lib/Service/FolderService.php +++ b/lib/Service/FolderService.php @@ -69,7 +69,7 @@ public function getFolder(): Folder { /** * @throws NotFoundException */ - public function getFileById(?int $nodeId = null): File { + public function getFileByNodeId(?int $nodeId = null): File { if ($this->getUserId()) { $file = $this->root->getUserFolder($this->getUserId())->getFirstNodeById($nodeId); diff --git a/lib/Service/SignFileService.php b/lib/Service/SignFileService.php index 8e7e6f5c43..d86ca88994 100644 --- a/lib/Service/SignFileService.php +++ b/lib/Service/SignFileService.php @@ -267,7 +267,7 @@ private function retrieveUserElement(FileElement $fileElement): int { } catch (MultipleObjectsReturnedException|DoesNotExistException|Exception) { throw new LibresignException($this->l10n->t('You need to define a visible signature or initials to sign this document.')); } - return $userElement->getFileId(); + return $userElement->getNodeId(); } private function bindFileElementWithTempFile(FileElement $fileElement, int $nodeId): VisibleElementAssoc { @@ -292,7 +292,7 @@ private function bindFileElementWithTempFile(FileElement $fileElement, int $node private function getNode(int $nodeId): ?File { if ($this->user instanceof IUser) { - return $this->folderService->getFileById($nodeId); + return $this->folderService->getFileByNodeId($nodeId); } $filesOfElementes = $this->signerElementsService->getElementsFromSession(); diff --git a/lib/Service/SignerElementsService.php b/lib/Service/SignerElementsService.php index e7e7d324f9..1076273a49 100644 --- a/lib/Service/SignerElementsService.php +++ b/lib/Service/SignerElementsService.php @@ -37,7 +37,7 @@ public function __construct( * @return LibresignUserElement */ public function getUserElementByNodeId(string $userId, int $nodeId): array { - $element = $this->userElementMapper->findOne(['file_id' => $nodeId, 'user_id' => $userId]); + $element = $this->userElementMapper->findOne(['node_id' => $nodeId, 'user_id' => $userId]); $exists = $this->signatureFileExists($element); if (!$exists) { throw new NotFoundException(); @@ -48,9 +48,9 @@ public function getUserElementByNodeId(string $userId, int $nodeId): array { 'file' => [ 'url' => $this->urlGenerator->linkToRoute('ocs.libresign.SignatureElements.getSignatureElementPreview', [ 'apiVersion' => 'v1', - 'nodeId' => $element->getFileId(), + 'nodeId' => $element->getNodeId(), ]), - 'nodeId' => $element->getFileId() + 'nodeId' => $element->getNodeId() ], 'userId' => $element->getUserId(), 'starred' => $element->getStarred() ? 1 : 0, @@ -75,9 +75,9 @@ public function getUserElements(string $userId): array { 'file' => [ 'url' => $this->urlGenerator->linkToRoute('ocs.libresign.SignatureElements.getSignatureElementPreview', [ 'apiVersion' => 'v1', - 'nodeId' => $element->getFileId(), + 'nodeId' => $element->getNodeId(), ]), - 'nodeId' => $element->getFileId() + 'nodeId' => $element->getNodeId() ], 'starred' => $element->getStarred() ? 1 : 0, 'userId' => $element->getUserId(), @@ -89,7 +89,7 @@ public function getUserElements(string $userId): array { private function signatureFileExists(UserElement $userElement): bool { try { - $this->folderService->getFileById($userElement->getFileId()); + $this->folderService->getFileByNodeId($userElement->getNodeId()); } catch (\Exception) { $this->userElementMapper->delete($userElement); return false; diff --git a/tests/php/Unit/Service/SignFileServiceTest.php b/tests/php/Unit/Service/SignFileServiceTest.php index 3b9b542f24..27896a8354 100644 --- a/tests/php/Unit/Service/SignFileServiceTest.php +++ b/tests/php/Unit/Service/SignFileServiceTest.php @@ -1029,7 +1029,7 @@ public function testSetVisibleElements( throw new DoesNotExistException('User element not found'); }); - $this->folderService->method('getFileById') + $this->folderService->method('getFileByNodeId') ->willReturnCallback(function ($id) use ($signatureFile) { if (isset($signatureFile[$id]) && $signatureFile[$id]['valid']) { $file = $this->getMockBuilder(\OCP\Files\File::class)->getMock();