Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.00% covered (warning)
80.00%
80 / 100
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileElementService
80.00% covered (warning)
80.00%
80 / 100
44.44% covered (danger)
44.44%
4 / 9
35.73
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
 saveVisibleElement
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getVisibleElementFromProperties
82.61% covered (warning)
82.61%
19 / 23
0.00% covered (danger)
0.00%
0 / 1
5.13
 translateCoordinatesToInternalNotation
65.71% covered (warning)
65.71%
23 / 35
0.00% covered (danger)
0.00%
0 / 1
17.80
 deleteVisibleElement
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 deleteVisibleElements
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getByFileIds
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVisibleElementsForSignRequest
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 formatVisibleElements
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service;
10
11use OCA\Libresign\Db\File;
12use OCA\Libresign\Db\FileElement;
13use OCA\Libresign\Db\FileElementMapper;
14use OCA\Libresign\Db\FileMapper;
15use OCA\Libresign\ResponseDefinitions;
16use OCP\AppFramework\Utility\ITimeFactory;
17
18/**
19 * @psalm-import-type LibresignVisibleElement from ResponseDefinitions
20 */
21class FileElementService {
22    public function __construct(
23        private FileMapper $fileMapper,
24        private FileElementMapper $fileElementMapper,
25        private ITimeFactory $timeFactory,
26    ) {
27    }
28
29    public function saveVisibleElement(array $element): FileElement {
30        $fileElement = $this->getVisibleElementFromProperties($element);
31        if ($fileElement->getId()) {
32            $this->fileElementMapper->update($fileElement);
33        } else {
34            $this->fileElementMapper->insert($fileElement);
35        }
36        return $fileElement;
37    }
38
39    private function getVisibleElementFromProperties(array $properties): FileElement {
40        if (!empty($properties['elementId'])) {
41            $fileElement = $this->fileElementMapper->getById($properties['elementId']);
42        } else {
43            $fileElement = new FileElement();
44            $fileElement->setCreatedAt($this->timeFactory->getDateTime());
45        }
46        $file = null;
47        if (!empty($properties['uuid'])) {
48            $file = $this->fileMapper->getByUuid($properties['uuid']);
49            $fileElement->setFileId($file->getId());
50        } elseif (!empty($properties['fileId'])) {
51            $file = $this->fileMapper->getById($properties['fileId']);
52            $fileElement->setFileId($properties['fileId']);
53        }
54        if (!$file) {
55            throw new \InvalidArgumentException('File not found for visible element');
56        }
57        $coordinates = $this->translateCoordinatesToInternalNotation($properties, $file);
58        $fileElement->setSignRequestId($properties['signRequestId']);
59        $fileElement->setType($properties['type']);
60        $fileElement->setPage($coordinates['page']);
61        $fileElement->setUrx($coordinates['urx']);
62        $fileElement->setUry($coordinates['ury']);
63        $fileElement->setLlx($coordinates['llx']);
64        $fileElement->setLly($coordinates['lly']);
65        $fileElement->setMetadata($properties['metadata'] ?? null);
66        return $fileElement;
67    }
68
69    private function translateCoordinatesToInternalNotation(array $properties, File $file): array {
70        $translated['page'] = $properties['coordinates']['page'] ?? 1;
71        $metadata = $file->getMetadata();
72        $dimension = $metadata['d'][$translated['page'] - 1];
73
74        if (isset($properties['coordinates']['ury'])) {
75            $translated['ury'] = $properties['coordinates']['ury'];
76        } elseif (isset($properties['coordinates']['top'])) {
77            $translated['ury'] = $dimension['h'] - $properties['coordinates']['top'];
78        } else {
79            $translated['ury'] = 0;
80        }
81
82        if (isset($properties['coordinates']['lly'])) {
83            $translated['lly'] = $properties['coordinates']['lly'];
84        } elseif (isset($properties['coordinates']['height'])) {
85            if ($properties['coordinates']['height'] > $translated['ury']) {
86                $translated['ury'] = $properties['coordinates']['height'];
87                $translated['lly'] = 0;
88            } else {
89                $translated['lly'] = $translated['ury'] - $properties['coordinates']['height'];
90            }
91        } else {
92            $translated['lly'] = 0;
93        }
94
95        if (isset($properties['coordinates']['llx'])) {
96            $translated['llx'] = $properties['coordinates']['llx'];
97        } elseif (isset($properties['coordinates']['left'])) {
98            $translated['llx'] = $properties['coordinates']['left'];
99        } else {
100            $translated['llx'] = 0;
101        }
102
103        if (isset($properties['coordinates']['urx'])) {
104            $translated['urx'] = $properties['coordinates']['urx'];
105        } elseif (isset($properties['coordinates']['width'])) {
106            $translated['urx'] = $translated['llx'] + $properties['coordinates']['width'];
107        } else {
108            $translated['urx'] = 0;
109        }
110        if ($translated['ury'] < $translated['lly']) {
111            $temp = $translated['ury'];
112            $translated['ury'] = $translated['lly'];
113            $translated['lly'] = $temp;
114        }
115        if ($translated['urx'] < $translated['llx']) {
116            $temp = $translated['urx'];
117            $translated['urx'] = $translated['llx'];
118            $translated['llx'] = $temp;
119        }
120
121        return $translated;
122    }
123
124    public function deleteVisibleElement(int $elementId): void {
125        $fileElement = new FileElement();
126        $fileElement = $fileElement->fromRow(['id' => $elementId]);
127        $this->fileElementMapper->delete($fileElement);
128    }
129
130    public function deleteVisibleElements(int $fileId): void {
131        $visibleElements = $this->fileElementMapper->getByFileId($fileId);
132        foreach ($visibleElements as $visibleElement) {
133            $this->fileElementMapper->delete($visibleElement);
134        }
135    }
136
137    /**
138     * @param int[] $fileIds
139     * @return FileElement[]
140     */
141    public function getByFileIds(array $fileIds): array {
142        return $this->fileElementMapper->getByFileIds($fileIds);
143    }
144
145    /**
146     * Return visible elements formatted for API responses for given file and signRequestId
147     *
148     * @psalm-return list<LibresignVisibleElement>
149     */
150    public function getVisibleElementsForSignRequest(File $file, int $signRequestId): array {
151        $rows = $this->fileElementMapper->getByFileIdAndSignRequestId($file->getId(), $signRequestId);
152        return $this->formatVisibleElements($rows, $file->getMetadata());
153    }
154
155    /**
156     * Format visible elements returned from DB rows for API responses.
157     *
158     * @param array<int, FileElement> $visibleElements Array of file elements as returned by mappers
159     * @param array $fileMetadata Metadata of the file (expects page dimensions under key 'd')
160     * @psalm-return list<LibresignVisibleElement>
161     */
162    public function formatVisibleElements(array $visibleElements, array $fileMetadata = []): array {
163        $result = [];
164        foreach ($visibleElements as $fileElement) {
165            $elementMetadata = $fileElement->getMetadata();
166            $metadata = $fileMetadata ?: (is_array($elementMetadata) ? $elementMetadata : []);
167            $dimension = $metadata['d'][$fileElement->getPage() - 1] ?? ['h' => 0];
168            $height = (int)abs($fileElement->getUry() - $fileElement->getLly());
169            $width = (int)abs($fileElement->getUrx() - $fileElement->getLlx());
170            $top = (int)abs($dimension['h'] - $fileElement->getUry());
171            $left = (int)$fileElement->getLlx();
172            $result[] = [
173                'elementId' => $fileElement->getId(),
174                'signRequestId' => $fileElement->getSignRequestId(),
175                'fileId' => $fileElement->getFileId(),
176                'type' => $fileElement->getType(),
177                'coordinates' => [
178                    'page' => $fileElement->getPage(),
179                    'urx' => $fileElement->getUrx(),
180                    'ury' => $fileElement->getUry(),
181                    'llx' => (int)$fileElement->getLlx(),
182                    'lly' => (int)$fileElement->getLly(),
183                    'left' => $left,
184                    'top' => $top,
185                    'width' => $width,
186                    'height' => $height,
187                ],
188            ];
189        }
190        return $result;
191    }
192}