Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.00% covered (warning)
88.00%
44 / 50
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileContentProvider
88.00% covered (warning)
88.00%
44 / 50
80.00% covered (warning)
80.00%
4 / 5
19.62
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
 getContentFromUrl
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 getContentFromBase64
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 getContentFromData
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getContentFromLibresignFile
60.00% covered (warning)
60.00%
9 / 15
0.00% covered (danger)
0.00%
0 / 1
6.60
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\Service\FolderService;
14use OCP\Http\Client\IClientService;
15use OCP\IL10N;
16use Psr\Log\LoggerInterface;
17
18class FileContentProvider {
19    public function __construct(
20        private IClientService $client,
21        private MimeService $mimeService,
22        private FolderService $folderService,
23        private LoggerInterface $logger,
24        private IL10N $l10n,
25    ) {
26    }
27
28    /**
29     * Get file content from a URL
30     *
31     * @throws \Exception if URL is invalid or content cannot be retrieved
32     */
33    public function getContentFromUrl(string $url): string {
34        if (!filter_var($url, FILTER_VALIDATE_URL)) {
35            // TRANSLATORS Error shown when a document URL provided for signing is invalid.
36            throw new LibresignException($this->l10n->t('Invalid URL file'), 422);
37        }
38
39        try {
40            $response = $this->client->newClient()->get($url);
41        } catch (\Throwable) {
42            // TRANSLATORS Error shown when a document URL provided for signing is invalid.
43            throw new LibresignException($this->l10n->t('Invalid URL file'), 422);
44        }
45
46        $content = (string)$response->getBody();
47
48        if (!$content) {
49            // TRANSLATORS Error shown when the downloaded or provided document content is empty.
50            throw new LibresignException($this->l10n->t('Empty file'), 422);
51        }
52
53        $mimetypeFromHeader = $response->getHeader('Content-Type');
54        // Strip parameters like "; charset=utf-8"
55        if (str_contains($mimetypeFromHeader, ';')) {
56            $mimetypeFromHeader = trim(explode(';', $mimetypeFromHeader)[0]);
57        }
58
59        $mimeTypeFromContent = $this->mimeService->getMimeType($content);
60
61        // application/octet-stream is a generic fallback — trust content detection
62        if ($mimetypeFromHeader !== 'application/octet-stream' && $mimetypeFromHeader !== $mimeTypeFromContent) {
63            // TRANSLATORS Error shown when a document URL provided for signing is invalid.
64            throw new LibresignException($this->l10n->t('Invalid URL file'), 422);
65        }
66
67        return $content;
68    }
69
70    /**
71     * Decode base64 content and validate MIME type
72     *
73     * @throws \Exception if MIME types don't match
74     */
75    public function getContentFromBase64(string $base64): string {
76        $withMime = explode(',', $base64);
77
78        if (count($withMime) === 2) {
79            $withMime[0] = explode(';', $withMime[0]);
80            $withMime[0][0] = explode(':', $withMime[0][0]);
81            $mimeTypeFromType = $withMime[0][0][1];
82
83            $base64 = $withMime[1];
84
85            $content = base64_decode($base64);
86            $mimeTypeFromContent = $this->mimeService->getMimeType($content);
87
88            if ($mimeTypeFromType !== $mimeTypeFromContent) {
89                // TRANSLATORS Error shown when a document URL provided for signing is invalid.
90                throw new LibresignException($this->l10n->t('Invalid URL file'), 422);
91            }
92
93            $this->mimeService->setMimeType($mimeTypeFromContent);
94        } else {
95            $content = base64_decode($base64);
96            $this->mimeService->getMimeType($content);
97        }
98
99        return $content;
100    }
101
102    /**
103     * Get raw file content from URL or base64 data array
104     *
105     * @param array $data Data array containing 'file' with 'url' or 'base64'
106     * @return string File content
107     * @throws \Exception if data is invalid
108     */
109    public function getContentFromData(array $data): string {
110        if (!empty($data['file']['url'])) {
111            return $this->getContentFromUrl($data['file']['url']);
112        }
113
114        if (!empty($data['file']['base64'])) {
115            return $this->getContentFromBase64($data['file']['base64']);
116        }
117
118        // TRANSLATORS Error shown when creating a signature request without any document source (upload, path, URL, or file id).
119        throw new LibresignException($this->l10n->t('No file source provided'), 422);
120    }
121
122    /**
123     * Get file content from a LibreSign File entity
124     *
125     * @param \OCA\Libresign\Db\File $file
126     * @throws LibresignException
127     */
128    public function getContentFromLibresignFile(\OCA\Libresign\Db\File $file): string {
129        try {
130            $nodeId = $file->getSignedNodeId();
131            if (!$nodeId) {
132                $nodeId = $file->getNodeId();
133            }
134
135            $fileNode = $this->folderService->getReadableNodeById($file->getUserId(), $nodeId);
136
137            if (!$fileNode instanceof \OCP\Files\File) {
138                // TRANSLATORS Error shown when the requested document cannot be found.
139                throw new LibresignException($this->l10n->t('File not found'), 404);
140            }
141
142            return $fileNode->getContent();
143        } catch (LibresignException $e) {
144            throw $e;
145        } catch (\Throwable $e) {
146            $this->logger->error('Failed to get file content: ' . $e->getMessage(), [
147                'fileId' => $file->getId(),
148                'exception' => $e,
149            ]);
150            // TRANSLATORS Error shown when LibreSign cannot validate the uploaded or referenced document for signing.
151            throw new LibresignException($this->l10n->t('Invalid data to validate file'), 404, $e);
152        }
153    }
154}