diff --git a/.gitignore b/.gitignore index c17998a276..21f0b2e4d7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later .idea .vscode +.phpactor* .env .secrets *.iml diff --git a/appinfo/info.xml b/appinfo/info.xml index 9e3287b989..1a9082ebef 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -28,6 +28,9 @@ Developed with ❤️ by [LibreCode](https://librecode.coop). Help us transform 12.0.0-dev.1 agpl LibreCode + + + https://github.com/LibreSign/libresign/blob/master/README.md @@ -90,4 +93,9 @@ Developed with ❤️ by [LibreCode](https://librecode.coop). Help us transform libresign.page.index + + + OCA\Libresign\Dav\SignatureStatusPlugin + + diff --git a/lib/Controller/FileController.php b/lib/Controller/FileController.php index 15a17ec7af..401e0a97ef 100644 --- a/lib/Controller/FileController.php +++ b/lib/Controller/FileController.php @@ -236,7 +236,7 @@ private function validate(?string $type = null, $identifier = null): DataRespons * List account files that need to be approved * * @param string|null $signer_uuid Signer UUID - * @param string|null $nodeId The nodeId (also called fileId). Is the id of a file at Nextcloud + * @param list|null $nodeIds The list of nodeIds (also called fileIds). It's the ids of files at Nextcloud * @param list|null $status Status could be none or many of 0 = draft, 1 = able to sign, 2 = partial signed, 3 = signed, 4 = deleted. * @param int|null $page the number of page to return * @param int|null $length Total of elements to return @@ -255,7 +255,7 @@ public function list( ?int $page = null, ?int $length = null, ?string $signer_uuid = null, - ?string $nodeId = null, + ?array $nodeIds = null, ?array $status = null, ?int $start = null, ?int $end = null, @@ -264,7 +264,7 @@ public function list( ): DataResponse { $filter = array_filter([ 'signer_uuid' => $signer_uuid, - 'nodeId' => $nodeId, + 'nodeIds' => $nodeIds, 'status' => $status, 'start' => $start, 'end' => $end, diff --git a/lib/Dav/SignatureStatusPlugin.php b/lib/Dav/SignatureStatusPlugin.php new file mode 100644 index 0000000000..d12bdceb2e --- /dev/null +++ b/lib/Dav/SignatureStatusPlugin.php @@ -0,0 +1,39 @@ +server = $server; + $server->on('propFind', [$this, 'propFind']); + } + + public function propFind(PropFind $propFind, INode $node): void { + if ($node instanceof File) { + $fileService = OC::$server->get(FileService::class); + $nodeId = $node->getId(); + + if ($fileService->isLibresignFile($nodeId)) { + $fileService->setFileByType('FileId', $nodeId); + + $propFind->handle('{http://nextcloud.org/ns}signature-status', $fileService->getStatus()); + $propFind->handle('{http://nextcloud.org/ns}signed-node-id', $fileService->getSignedNodeId()); + } + } + } +} diff --git a/lib/Db/FileMapper.php b/lib/Db/FileMapper.php index a43c276135..d5a2f552b8 100644 --- a/lib/Db/FileMapper.php +++ b/lib/Db/FileMapper.php @@ -167,6 +167,30 @@ public function getByFileId(?int $nodeId = null): File { return $file; } + /** + * Check if file exists + */ + public function fileIdExists(int $nodeId): bool { + $exists = array_filter($this->file, fn ($f) => $f->getNodeId() === $nodeId || $f->getSignedNodeId() === $nodeId); + if (!empty($exists)) { + return true; + } + + $qb = $this->db->getQueryBuilder(); + + $qb->select('id') + ->from($this->getTableName()) + ->where( + $qb->expr()->orX( + $qb->expr()->eq('node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('signed_node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT)) + ) + ); + + $files = $this->findEntities($qb); + return !empty($files); + } + /** * @return File[] */ diff --git a/lib/Db/SignRequestMapper.php b/lib/Db/SignRequestMapper.php index 404a685586..97d311b475 100644 --- a/lib/Db/SignRequestMapper.php +++ b/lib/Db/SignRequestMapper.php @@ -433,6 +433,7 @@ private function getFilesAssociatedFilesWithMeQueryBuilder(string $userId, array $qb->select( 'f.id', 'f.node_id', + 'f.signed_node_id', 'f.user_id', 'f.uuid', 'f.name', @@ -443,6 +444,7 @@ private function getFilesAssociatedFilesWithMeQueryBuilder(string $userId, array ->groupBy( 'f.id', 'f.node_id', + 'f.signed_node_id', 'f.user_id', 'f.uuid', 'f.name', @@ -482,9 +484,9 @@ private function getFilesAssociatedFilesWithMeQueryBuilder(string $userId, array $qb->expr()->eq('sr.uuid', $qb->createNamedParameter($filter['signer_uuid'])) ); } - if (!empty($filter['nodeId'])) { + if (!empty($filter['nodeIds'])) { $qb->andWhere( - $qb->expr()->eq('f.node_id', $qb->createNamedParameter($filter['nodeId'], IQueryBuilder::PARAM_INT)) + $qb->expr()->in('f.node_id', $qb->createNamedParameter($filter['nodeIds'], IQueryBuilder::PARAM_STR_ARRAY)) ); } if (!empty($filter['status'])) { @@ -544,6 +546,7 @@ private function formatListRow(array $row): array { $row['status'] = (int)$row['status']; $row['statusText'] = $this->fileMapper->getTextOfStatus($row['status']); $row['nodeId'] = (int)$row['node_id']; + $row['signedNodeId'] = (int)$row['signed_node_id']; $row['requested_by'] = [ 'userId' => $row['user_id'], 'displayName' => $this->userManager->get($row['user_id'])?->getDisplayName(), @@ -554,6 +557,7 @@ private function formatListRow(array $row): array { unset( $row['user_id'], $row['node_id'], + $row['signed_node_id'], ); return $row; } diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index be08829590..9c7b1e32d3 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -217,6 +217,7 @@ * file: array{ * type: string, * nodeId: non-negative-int, + * signedNodeId: non-negative-int, * url: string, * }, * callback: ?string, diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index 9aad4da827..09f2d9a061 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -250,6 +250,19 @@ private function getFile(): \OCP\Files\File { return current($fileToValidate); } + public function getStatus(): int { + return $this->file->getStatus(); + } + + public function getSignedNodeId(): ?int { + $status = $this->file->getStatus(); + + if (!in_array($status, [File::STATUS_PARTIAL_SIGNED, File::STATUS_SIGNED])) { + return null; + } + return $this->file->getSignedNodeId(); + } + private function getFileContent(): string { if ($this->fileContent) { return $this->fileContent; @@ -265,6 +278,14 @@ private function getFileContent(): string { return ''; } + public function isLibresignFile(int $nodeId): bool { + try { + return $this->fileMapper->fileIdExists($nodeId); + } catch (\Throwable) { + throw new LibresignException($this->l10n->t('Invalid data to validate file'), 404); + } + } + private function loadFileMetadata(): void { if (!$content = $this->getFileContent()) { return; diff --git a/openapi-full.json b/openapi-full.json index 70617906ab..240ff6e381 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -343,6 +343,7 @@ "required": [ "type", "nodeId", + "signedNodeId", "url" ], "properties": { @@ -354,6 +355,11 @@ "format": "int64", "minimum": 0 }, + "signedNodeId": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, "url": { "type": "string" } @@ -4118,12 +4124,15 @@ } }, { - "name": "nodeId", + "name": "nodeIds[]", "in": "query", - "description": "The nodeId (also called fileId). Is the id of a file at Nextcloud", + "description": "The list of nodeIds (also called fileIds). It's the ids of files at Nextcloud", "schema": { - "type": "string", - "nullable": true + "type": "array", + "nullable": true, + "items": { + "type": "string" + } } }, { diff --git a/openapi.json b/openapi.json index 8c2104f810..c2e9d0903a 100644 --- a/openapi.json +++ b/openapi.json @@ -273,6 +273,7 @@ "required": [ "type", "nodeId", + "signedNodeId", "url" ], "properties": { @@ -284,6 +285,11 @@ "format": "int64", "minimum": 0 }, + "signedNodeId": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, "url": { "type": "string" } @@ -4000,12 +4006,15 @@ } }, { - "name": "nodeId", + "name": "nodeIds[]", "in": "query", - "description": "The nodeId (also called fileId). Is the id of a file at Nextcloud", + "description": "The list of nodeIds (also called fileIds). It's the ids of files at Nextcloud", "schema": { - "type": "string", - "nullable": true + "type": "array", + "nullable": true, + "items": { + "type": "string" + } } }, { diff --git a/src/actions/showStatusInlineAction.js b/src/actions/showStatusInlineAction.js new file mode 100644 index 0000000000..defbf52ac1 --- /dev/null +++ b/src/actions/showStatusInlineAction.js @@ -0,0 +1,46 @@ +/** + * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { FileAction, registerFileAction } from '@nextcloud/files' +import { loadState } from '@nextcloud/initial-state' +import { t } from '@nextcloud/l10n' + +import { SIGN_STATUS } from '../domains/sign/enum.js' +import { fileStatus } from '../helpers/fileStatus.js' + +const action = new FileAction({ + id: 'show-status-inline', + displayName: () => '', + title: (nodes) => { + const node = nodes[0] + + const signedNodeId = node.attributes['signed-node-id'] + + return !signedNodeId || node.fileid === signedNodeId + ? fileStatus.find(status => status.id === node.attributes['signature-status']).label + : t('libresign', 'original file') + }, + exec: async () => null, + iconSvgInline: (nodes) => { + const node = nodes[0] + + const signedNodeId = node.attributes['signed-node-id'] + + return !signedNodeId || node.fileid === signedNodeId + ? fileStatus.find(status => status.id === node.attributes['signature-status']).icon + : fileStatus.find(status => status.id === SIGN_STATUS.ABLE_TO_SIGN).icon + }, + inline: () => true, + enabled: (nodes) => { + return loadState('libresign', 'certificate_ok') + && nodes.length > 0 + && nodes + .map(node => node.mime) + .every(mime => mime === 'application/pdf') + && nodes.every(node => node.attributes['signature-status']) + }, + order: -1, +}) + +registerFileAction(action) diff --git a/src/init.js b/src/init.js index 94851e5a94..d97dca5cf4 100644 --- a/src/init.js +++ b/src/init.js @@ -6,7 +6,7 @@ import Vue from 'vue' import axios from '@nextcloud/axios' -import { addNewFileMenuEntry, Permission } from '@nextcloud/files' +import { addNewFileMenuEntry, Permission, registerDavProperty } from '@nextcloud/files' import { translate, translatePlural } from '@nextcloud/l10n' import { generateOcsUrl } from '@nextcloud/router' import { getUploader } from '@nextcloud/upload' @@ -21,6 +21,9 @@ Vue.prototype.n = translatePlural Vue.prototype.OC = OC Vue.prototype.OCA = OCA +registerDavProperty('nc:signature-status', { nc: 'http://nextcloud.org/ns' }) +registerDavProperty('nc:signed-node-id', { nc: 'http://nextcloud.org/ns' }) + addNewFileMenuEntry({ id: 'libresign-request', displayName: t('libresign', 'New signature request'), diff --git a/src/store/files.js b/src/store/files.js index 2c18c7af2b..092f5c903e 100644 --- a/src/store/files.js +++ b/src/store/files.js @@ -71,7 +71,7 @@ export const useFilesStore = function(...args) { }, async flushSelectedFile() { const files = await this.getAllFiles({ - nodeId: this.selectedNodeId, + 'nodeIds[]': [this.selectedNodeId], }) this.addFile(files[this.selectedNodeId]) }, diff --git a/src/tab.js b/src/tab.js index 9c3cc6e4f1..37825f7190 100644 --- a/src/tab.js +++ b/src/tab.js @@ -12,6 +12,7 @@ import { translate, translatePlural } from '@nextcloud/l10n' import AppFilesTab from './Components/RightSidebar/AppFilesTab.vue' import './actions/openInLibreSignAction.js' +import './actions/showStatusInlineAction.js' import './plugins/vuelidate.js' import './style/icons.scss' diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index c330e6fe1f..4b3d5d39f9 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -1291,6 +1291,8 @@ export type components = { type: string; /** Format: int64 */ nodeId: number; + /** Format: int64 */ + signedNodeId: number; url: string; }; callback: string | null; @@ -2806,8 +2808,8 @@ export interface operations { length?: number | null; /** @description Signer UUID */ signer_uuid?: string | null; - /** @description The nodeId (also called fileId). Is the id of a file at Nextcloud */ - nodeId?: string | null; + /** @description The list of nodeIds (also called fileIds). It's the ids of files at Nextcloud */ + "nodeIds[]"?: string[] | null; /** @description Status could be none or many of 0 = draft, 1 = able to sign, 2 = partial signed, 3 = signed, 4 = deleted. */ "status[]"?: number[] | null; /** @description Start date of signature request (UNIX timestamp) */ diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 8edf3c8e0f..a41b78cbff 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1012,6 +1012,8 @@ export type components = { type: string; /** Format: int64 */ nodeId: number; + /** Format: int64 */ + signedNodeId: number; url: string; }; callback: string | null; @@ -2515,8 +2517,8 @@ export interface operations { length?: number | null; /** @description Signer UUID */ signer_uuid?: string | null; - /** @description The nodeId (also called fileId). Is the id of a file at Nextcloud */ - nodeId?: string | null; + /** @description The list of nodeIds (also called fileIds). It's the ids of files at Nextcloud */ + "nodeIds[]"?: string[] | null; /** @description Status could be none or many of 0 = draft, 1 = able to sign, 2 = partial signed, 3 = signed, 4 = deleted. */ "status[]"?: number[] | null; /** @description Start date of signature request (UNIX timestamp) */ diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 3198641746..fdc803325f 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -47,6 +47,11 @@ + + + + +