Skip to content
Merged
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
12 changes: 6 additions & 6 deletions lib/Db/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 = '';
Expand Down
38 changes: 0 additions & 38 deletions lib/Db/FileMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
198 changes: 160 additions & 38 deletions lib/Listener/BeforeNodeDeletedListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,78 +8,200 @@

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;

/**
* @template-implements IEventListener<Event|BeforeNodeDeletedEvent|CacheEntryRemovedEvent>
*/
class BeforeNodeDeletedListener implements IEventListener {
public function __construct(
private FileMapper $fileMapper,
private RequestSignatureService $requestSignatureService,
private IDBConnection $db,
) {
}

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();
}
}
36 changes: 36 additions & 0 deletions lib/Migration/Version17000Date20260106000000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Libresign\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version17000Date20260106000000 extends SimpleMigrationStep {
#[\Override]
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$changed = false;

if ($schema->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;
}
}
4 changes: 2 additions & 2 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
* }
Expand Down
Loading
Loading