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 @@
+
+
+
+
+