Skip to content

Commit e09cb34

Browse files
authored
Merge pull request #6303 from LibreSign/backport/6302/stable31
[stable31] feat: envelope custom path
2 parents 2eec537 + 0e64d49 commit e09cb34

15 files changed

Lines changed: 752 additions & 21 deletions

lib/Capabilities.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
namespace OCA\Libresign;
1010

11-
use OCA\Libresign\Service\EnvelopeService;
11+
use OCA\Libresign\Service\Envelope\EnvelopeService;
1212
use OCA\Libresign\Service\SignatureTextService;
1313
use OCA\Libresign\Service\SignerElementsService;
1414
use OCP\App\IAppManager;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Libresign\Service\Envelope;
10+
11+
use OCA\Libresign\Exception\LibresignException;
12+
use OCA\Libresign\Service\FolderService;
13+
use OCP\Files\Node;
14+
use OCP\IUser;
15+
16+
class EnvelopeFileRelocator {
17+
public function __construct(
18+
private FolderService $folderService,
19+
) {
20+
}
21+
22+
public function ensureFileInEnvelopeFolder(Node $sourceNode, int $envelopeFolderId, IUser $userManager): Node {
23+
$this->folderService->setUserId($userManager->getUID());
24+
$userRootFolder = $this->folderService->getUserRootFolder();
25+
$envelopeFolder = $userRootFolder->getFirstNodeById($envelopeFolderId);
26+
27+
if (!$envelopeFolder instanceof \OCP\Files\Folder) {
28+
throw new LibresignException('Envelope folder not found');
29+
}
30+
31+
if ($this->isNodeInsideFolder($sourceNode, $envelopeFolder)) {
32+
return $sourceNode;
33+
}
34+
35+
if (!$sourceNode instanceof \OCP\Files\File) {
36+
throw new LibresignException('Invalid file type for envelope');
37+
}
38+
39+
return $envelopeFolder->newFile($sourceNode->getName(), $sourceNode->getContent());
40+
}
41+
42+
private function isNodeInsideFolder(Node $node, \OCP\Files\Folder $folder): bool {
43+
return str_starts_with($node->getPath(), $folder->getPath() . '/');
44+
}
45+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Libresign\Service\Envelope;
10+
11+
use DateTime;
12+
use OCA\Libresign\AppInfo\Application;
13+
use OCA\Libresign\Db\File as FileEntity;
14+
use OCA\Libresign\Db\FileMapper;
15+
use OCA\Libresign\Enum\NodeType;
16+
use OCA\Libresign\Exception\LibresignException;
17+
use OCA\Libresign\Service\FolderService;
18+
use OCP\AppFramework\Db\DoesNotExistException;
19+
use OCP\IAppConfig;
20+
use OCP\IL10N;
21+
use Sabre\DAV\UUIDUtil;
22+
23+
class EnvelopeService {
24+
public function __construct(
25+
protected FileMapper $fileMapper,
26+
protected IL10N $l10n,
27+
protected IAppConfig $appConfig,
28+
protected FolderService $folderService,
29+
) {
30+
}
31+
32+
public function isEnabled(): bool {
33+
return $this->appConfig->getValueBool(Application::APP_ID, 'envelope_enabled', true);
34+
}
35+
36+
/**
37+
* @throws LibresignException
38+
*/
39+
public function validateEnvelopeConstraints(int $fileCount): void {
40+
if (!$this->isEnabled()) {
41+
throw new LibresignException($this->l10n->t('Envelope feature is disabled'));
42+
}
43+
44+
$maxFiles = $this->getMaxFilesPerEnvelope();
45+
if ($fileCount > $maxFiles) {
46+
throw new LibresignException(
47+
$this->l10n->t('Maximum number of files per envelope (%s) exceeded', [$maxFiles])
48+
);
49+
}
50+
}
51+
52+
public function createEnvelope(
53+
string $name,
54+
string $userId,
55+
int $filesCount = 0,
56+
?string $path = null,
57+
): FileEntity {
58+
$this->folderService->setUserId($userId);
59+
60+
$uuid = UUIDUtil::getUUID();
61+
if ($path) {
62+
$envelopeFolder = $this->folderService->getOrCreateFolderByAbsolutePath($path);
63+
} else {
64+
$parentFolder = $this->folderService->getFolder();
65+
$folderName = $name . '_' . $uuid;
66+
$envelopeFolder = $parentFolder->newFolder($folderName);
67+
}
68+
69+
$envelope = new FileEntity();
70+
$envelope->setNodeId($envelopeFolder->getId());
71+
$envelope->setNodeTypeEnum(NodeType::ENVELOPE);
72+
$envelope->setName($name);
73+
$envelope->setUuid($uuid);
74+
$envelope->setCreatedAt(new DateTime());
75+
$envelope->setStatus(FileEntity::STATUS_DRAFT);
76+
77+
$envelope->setMetadata(['filesCount' => $filesCount]);
78+
79+
if ($userId) {
80+
$envelope->setUserId($userId);
81+
}
82+
83+
return $this->fileMapper->insert($envelope);
84+
}
85+
86+
public function addFileToEnvelope(int $envelopeId, FileEntity $file): FileEntity {
87+
$envelope = $this->fileMapper->getById($envelopeId);
88+
89+
if (!$envelope->isEnvelope()) {
90+
throw new LibresignException($this->l10n->t('The specified ID is not an envelope'));
91+
}
92+
93+
if ($envelope->getStatus() > FileEntity::STATUS_DRAFT) {
94+
throw new LibresignException($this->l10n->t('Cannot add files to an envelope that is already in signing process'));
95+
}
96+
97+
$maxFiles = $this->getMaxFilesPerEnvelope();
98+
$currentCount = $this->fileMapper->countChildrenFiles($envelopeId);
99+
if ($currentCount >= $maxFiles) {
100+
throw new LibresignException(
101+
$this->l10n->t('Maximum number of files per envelope (%s) exceeded', [$maxFiles])
102+
);
103+
}
104+
105+
$file->setParentFileId($envelopeId);
106+
$file->setNodeTypeEnum(NodeType::FILE);
107+
108+
return $this->fileMapper->update($file);
109+
}
110+
111+
public function getEnvelopeByFileId(int $fileId): ?FileEntity {
112+
try {
113+
return $this->fileMapper->getParentEnvelope($fileId);
114+
} catch (DoesNotExistException) {
115+
return null;
116+
}
117+
}
118+
119+
public function getEnvelopeFolder(FileEntity $envelope): \OCP\Files\Folder {
120+
$userId = $envelope->getUserId();
121+
if (!$userId) {
122+
throw new LibresignException('Envelope does not have a user');
123+
}
124+
125+
$this->folderService->setUserId($userId);
126+
$userRootFolder = $this->folderService->getUserRootFolder();
127+
128+
$envelopeFolderNode = $userRootFolder->getFirstNodeById($envelope->getNodeId());
129+
if (!$envelopeFolderNode instanceof \OCP\Files\Folder) {
130+
throw new LibresignException('Envelope folder not found');
131+
}
132+
133+
return $envelopeFolderNode;
134+
}
135+
136+
private function getMaxFilesPerEnvelope(): int {
137+
return $this->appConfig->getValueInt(Application::APP_ID, 'envelope_max_files', 50);
138+
}
139+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Libresign\Service;
10+
11+
use OCA\Libresign\Exception\LibresignException;
12+
use OCP\Files\Node;
13+
use OCP\IUser;
14+
15+
class EnvelopeFileRelocator {
16+
public function __construct(
17+
private FolderService $folderService,
18+
) {
19+
}
20+
21+
public function ensureFileInEnvelopeFolder(Node $sourceNode, int $envelopeFolderId, IUser $userManager): Node {
22+
$this->folderService->setUserId($userManager->getUID());
23+
$userRootFolder = $this->folderService->getUserRootFolder();
24+
$envelopeFolder = $userRootFolder->getFirstNodeById($envelopeFolderId);
25+
26+
if (!$envelopeFolder instanceof \OCP\Files\Folder) {
27+
throw new LibresignException('Envelope folder not found');
28+
}
29+
30+
if ($this->isNodeInsideFolder($sourceNode, $envelopeFolder)) {
31+
return $sourceNode;
32+
}
33+
34+
if (!$sourceNode instanceof \OCP\Files\File) {
35+
throw new LibresignException('Invalid file type for envelope');
36+
}
37+
38+
return $envelopeFolder->newFile($sourceNode->getName(), $sourceNode->getContent());
39+
}
40+
41+
private function isNodeInsideFolder(Node $node, \OCP\Files\Folder $folder): bool {
42+
return str_starts_with($node->getPath(), $folder->getPath() . '/');
43+
}
44+
}

lib/Service/EnvelopeService.php

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,22 @@ public function validateEnvelopeConstraints(int $fileCount): void {
4848
}
4949
}
5050

51-
public function createEnvelope(string $name, string $userId, int $filesCount = 0): FileEntity {
51+
public function createEnvelope(
52+
string $name,
53+
string $userId,
54+
int $filesCount = 0,
55+
?string $path = null,
56+
): FileEntity {
5257
$this->folderService->setUserId($userId);
5358

54-
$parentFolder = $this->folderService->getFolder();
55-
5659
$uuid = UUIDUtil::getUUID();
57-
$folderName = $name . '_' . $uuid;
58-
$envelopeFolder = $parentFolder->newFolder($folderName);
60+
if ($path) {
61+
$envelopeFolder = $this->folderService->getOrCreateFolderByAbsolutePath($path);
62+
} else {
63+
$parentFolder = $this->folderService->getFolder();
64+
$folderName = $name . '_' . $uuid;
65+
$envelopeFolder = $parentFolder->newFolder($folderName);
66+
}
5967

6068
$envelope = new FileEntity();
6169
$envelope->setNodeId($envelopeFolder->getId());
@@ -114,9 +122,9 @@ public function getEnvelopeFolder(FileEntity $envelope): \OCP\Files\Folder {
114122
}
115123

116124
$this->folderService->setUserId($userId);
117-
$userFolder = $this->folderService->getFolder();
125+
$userRootFolder = $this->folderService->getUserRootFolder();
118126

119-
$envelopeFolderNode = $userFolder->getFirstNodeById($envelope->getNodeId());
127+
$envelopeFolderNode = $userRootFolder->getFirstNodeById($envelope->getNodeId());
120128
if (!$envelopeFolderNode instanceof \OCP\Files\Folder) {
121129
throw new LibresignException('Envelope folder not found');
122130
}

lib/Service/FileService.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use OCA\Libresign\Handler\SignEngine\Pkcs12Handler;
2222
use OCA\Libresign\Helper\FileUploadHelper;
2323
use OCA\Libresign\ResponseDefinitions;
24+
use OCA\Libresign\Service\Envelope\EnvelopeService;
2425
use OCA\Libresign\Service\File\CertificateChainService;
2526
use OCA\Libresign\Service\File\EnvelopeAssembler;
2627
use OCA\Libresign\Service\File\EnvelopeProgressService;

lib/Service/FolderService.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@ public function getUserId(): ?string {
4545
return $this->userId;
4646
}
4747

48+
/**
49+
* Get the user's root folder (full home), not the LibreSign container.
50+
*
51+
* @throws LibresignException
52+
*/
53+
public function getUserRootFolder(): Folder {
54+
if (!$this->userId) {
55+
throw new LibresignException('Invalid user to resolve folder');
56+
}
57+
58+
return $this->root->getUserFolder($this->userId);
59+
}
60+
4861
/**
4962
* Get folder for user and creates it if non-existent
5063
*
@@ -200,4 +213,50 @@ public function getFileByPath(string $path): Node {
200213
throw new LibresignException($this->l10n->t('Invalid data to validate file'), 404);
201214
}
202215
}
216+
217+
/**
218+
* Ensure a folder exists at a given absolute user path, creating missing segments.
219+
* If the final folder already exists, it must be empty.
220+
*
221+
* @throws LibresignException
222+
*/
223+
public function getOrCreateFolderByAbsolutePath(string $path): Folder {
224+
if (!$this->userId) {
225+
throw new LibresignException('Invalid user to create envelope folder');
226+
}
227+
228+
$cleanPath = ltrim($path, '/');
229+
$userFolder = $this->root->getUserFolder($this->userId);
230+
231+
if ($cleanPath === '') {
232+
return $userFolder;
233+
}
234+
235+
$segments = array_filter(explode('/', $cleanPath), static fn (string $segment) => $segment !== '');
236+
$folder = $userFolder;
237+
$isLastSegment = false;
238+
239+
foreach ($segments as $index => $segment) {
240+
$isLastSegment = ($index === count($segments) - 1);
241+
242+
try {
243+
$node = $folder->get($segment);
244+
if (!$node instanceof Folder) {
245+
throw new LibresignException('Invalid folder path');
246+
}
247+
$folder = $node;
248+
249+
if ($isLastSegment) {
250+
$contents = $folder->getDirectoryListing();
251+
if (count($contents) > 0) {
252+
throw new LibresignException($this->l10n->t('Folder already exists and is not empty: %s', [$path]));
253+
}
254+
}
255+
} catch (NotFoundException) {
256+
$folder = $folder->newFolder($segment);
257+
}
258+
}
259+
260+
return $folder;
261+
}
203262
}

0 commit comments

Comments
 (0)