Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.83% covered (success)
97.83%
45 / 46
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
UploadProcessor
97.83% covered (success)
97.83%
45 / 46
80.00% covered (warning)
80.00%
4 / 5
12
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNodeFromUploadedFile
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 processUploadedFilesWithRollback
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
3
 validateFileContent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 rollbackCreatedNodes
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5/**
6 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
7 * SPDX-License-Identifier: AGPL-3.0-or-later
8 */
9
10namespace OCA\Libresign\Service\File;
11
12use OCA\Libresign\Exception\LibresignException;
13use OCA\Libresign\Helper\FileUploadHelper;
14use OCA\Libresign\Service\File\Pdf\PdfValidator;
15use OCA\Libresign\Service\FolderService;
16use OCA\Libresign\Service\Validation\FileInputValidator;
17use OCP\Files\Node;
18use OCP\IUser;
19use Psr\Log\LoggerInterface;
20
21class UploadProcessor {
22    public function __construct(
23        private FileUploadHelper $uploadHelper,
24        private FolderService $folderService,
25        private MimeService $mimeService,
26        private PdfValidator $pdfValidator,
27        private FileInputValidator $fileInputValidator,
28        private LoggerInterface $logger,
29    ) {
30    }
31
32    /**
33     * Get Node from uploaded file data
34     *
35     * @param array $data Must contain 'uploadedFile', 'userManager', 'name', and optionally 'settings'
36     * @return Node The created file node
37     * @throws LibresignException
38     */
39    public function getNodeFromUploadedFile(array $data): Node {
40        if (!$this->folderService->getUserId()) {
41            $this->folderService->setUserId($data['userManager']->getUID());
42        }
43
44        $uploadedFile = $data['uploadedFile'];
45
46        $this->uploadHelper->validateUploadedFile($uploadedFile);
47        $content = $this->uploadHelper->readUploadedFile($uploadedFile);
48
49        $extension = $this->mimeService->getExtension($content);
50        $this->validateFileContent($content, $data['name'], $extension);
51
52        $folderToFile = $this->folderService->getFolderForFile($data, $data['userManager']);
53        if (!$folderToFile instanceof \OCP\Files\Folder) {
54            throw new LibresignException('Envelope folder not found');
55        }
56
57        @unlink($uploadedFile['tmp_name']);
58
59        return $folderToFile->newFile($data['name'] . '.' . $extension, $content);
60    }
61
62    /**
63     * Process multiple uploaded files with automatic rollback on error
64     *
65     * @param array $filesArray Normalized array of uploaded files
66     * @param IUser $user User who is uploading
67     * @param array $settings Upload settings
68     * @return list<array{fileNode: Node, name: string}>
69     * @throws LibresignException
70     */
71    public function processUploadedFilesWithRollback(array $filesArray, IUser $user, array $settings): array {
72        $processedFiles = [];
73        $createdNodes = [];
74        $shouldRollback = true;
75
76        try {
77            foreach ($filesArray as $uploadedFile) {
78                $fileName = pathinfo((string)$uploadedFile['name'], PATHINFO_FILENAME);
79
80                $node = $this->getNodeFromUploadedFile([
81                    'userManager' => $user,
82                    'name' => $fileName,
83                    'uploadedFile' => $uploadedFile,
84                    'settings' => $settings,
85                ]);
86
87                $createdNodes[] = $node;
88
89                $this->fileInputValidator->validateNewFile([
90                    'file' => ['fileId' => $node->getId()],
91                    'userManager' => $user,
92                ]);
93
94                $processedFiles[] = [
95                    'fileNode' => $node,
96                    'name' => $fileName,
97                ];
98            }
99
100            $shouldRollback = false;
101            return $processedFiles;
102        } finally {
103            if ($shouldRollback) {
104                $this->rollbackCreatedNodes($createdNodes);
105            }
106        }
107    }
108
109    /**
110     * Validate file content based on extension
111     *
112     * @param string $content The file content
113     * @param string $fileName File name for error messages
114     * @param string $extension The file extension
115     * @throws LibresignException
116     */
117    private function validateFileContent(string $content, string $fileName, string $extension): void {
118        if ($extension === 'pdf') {
119            $this->pdfValidator->validate($content, $fileName);
120        }
121    }
122
123    /**
124     * Rollback created nodes on error
125     *
126     * @param Node[] $nodes
127     */
128    private function rollbackCreatedNodes(array $nodes): void {
129        foreach ($nodes as $node) {
130            try {
131                $node->delete();
132            } catch (\Exception $deleteError) {
133                $this->logger->error('Failed to rollback uploaded file', [
134                    'nodeId' => $node->getId(),
135                    'error' => $deleteError->getMessage(),
136                ]);
137            }
138        }
139    }
140}