Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
92.31% |
12 / 13 |
CRAP | |
95.74% |
45 / 47 |
Attachment | |
0.00% |
0 / 1 |
|
92.31% |
12 / 13 |
47 | |
95.74% |
45 / 47 |
setId | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getId | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getLives | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setLives | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setFilehash | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getFilehash | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setMimetype | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getMimetype | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setFilename | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getFilename | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setSize | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getSize | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setWidth | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getWidth | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setHeight | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getHeight | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
setModified | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getModified | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getMimetypeMajor | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
getMimetypeMinor | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 2 |
|||
livesIncrementAndGet | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
livesDecrementAndGet | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
kill | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
deleteStorage | |
100.00% |
1 / 1 |
4 | |
100.00% |
8 / 8 |
|||
delete | |
100.00% |
1 / 1 |
7 | |
100.00% |
13 / 13 |
|||
getBestTitle | |
100.00% |
1 / 1 |
4 | |
100.00% |
9 / 9 |
|||
getThumbnails | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getPath | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
getUrl | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getThumbnailUrl | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
schemaDef | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
1 | <?php |
2 | |
3 | // {{{ License |
4 | |
5 | // This file is part of GNU social - https://www.gnu.org/software/social |
6 | // |
7 | // GNU social is free software: you can redistribute it and/or modify |
8 | // it under the terms of the GNU Affero General Public License as published by |
9 | // the Free Software Foundation, either version 3 of the License, or |
10 | // (at your option) any later version. |
11 | // |
12 | // GNU social is distributed in the hope that it will be useful, |
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | // GNU Affero General Public License for more details. |
16 | // |
17 | // You should have received a copy of the GNU Affero General Public License |
18 | // along with GNU social. If not, see <http://www.gnu.org/licenses/>. |
19 | |
20 | // }}} |
21 | |
22 | namespace App\Entity; |
23 | |
24 | use App\Core\DB\DB; |
25 | use App\Core\Entity; |
26 | use App\Core\GSFile; |
27 | use function App\Core\I18n\_m; |
28 | use App\Core\Log; |
29 | use App\Core\Router\Router; |
30 | use App\Util\Common; |
31 | use App\Util\Exception\DuplicateFoundException; |
32 | use App\Util\Exception\NotFoundException; |
33 | use App\Util\Exception\ServerException; |
34 | use DateTimeInterface; |
35 | |
36 | /** |
37 | * Entity for uploaded files |
38 | * |
39 | * @category DB |
40 | * @package GNUsocial |
41 | * |
42 | * @author Zach Copley <zach@status.net> |
43 | * @copyright 2010 StatusNet Inc. |
44 | * @author Mikael Nordfeldth <mmn@hethane.se> |
45 | * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org |
46 | * @author Hugo Sales <hugo@hsal.es> |
47 | * @author Diogo Peralta Cordeiro <mail@diogo.site> |
48 | * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org |
49 | * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later |
50 | */ |
51 | class Attachment extends Entity |
52 | { |
53 | // {{{ Autocode |
54 | // @codeCoverageIgnoreStart |
55 | private int $id; |
56 | private int $lives = 1; |
57 | private ?string $filehash; |
58 | private ?string $mimetype; |
59 | private ?string $filename; |
60 | private ?int $size; |
61 | private ?int $width; |
62 | private ?int $height; |
63 | private DateTimeInterface $modified; |
64 | |
65 | public function setId(int $id): self |
66 | { |
67 | $this->id = $id; |
68 | return $this; |
69 | } |
70 | |
71 | public function getId(): int |
72 | { |
73 | return $this->id; |
74 | } |
75 | |
76 | /** |
77 | * @return int |
78 | */ |
79 | public function getLives(): int |
80 | { |
81 | return $this->lives; |
82 | } |
83 | |
84 | /** |
85 | * @param int $lives |
86 | */ |
87 | public function setLives(int $lives): void |
88 | { |
89 | $this->lives = $lives; |
90 | } |
91 | |
92 | public function setFilehash(?string $filehash): self |
93 | { |
94 | $this->filehash = $filehash; |
95 | return $this; |
96 | } |
97 | |
98 | public function getFilehash(): ?string |
99 | { |
100 | return $this->filehash; |
101 | } |
102 | |
103 | public function setMimetype(?string $mimetype): self |
104 | { |
105 | $this->mimetype = $mimetype; |
106 | return $this; |
107 | } |
108 | |
109 | public function getMimetype(): ?string |
110 | { |
111 | return $this->mimetype; |
112 | } |
113 | |
114 | public function setFilename(?string $filename): self |
115 | { |
116 | $this->filename = $filename; |
117 | return $this; |
118 | } |
119 | |
120 | public function getFilename(): ?string |
121 | { |
122 | return $this->filename; |
123 | } |
124 | |
125 | public function setSize(?int $size): self |
126 | { |
127 | $this->size = $size; |
128 | return $this; |
129 | } |
130 | |
131 | public function getSize(): ?int |
132 | { |
133 | return $this->size; |
134 | } |
135 | |
136 | public function setWidth(?int $width): self |
137 | { |
138 | $this->width = $width; |
139 | return $this; |
140 | } |
141 | |
142 | public function getWidth(): ?int |
143 | { |
144 | return $this->width; |
145 | } |
146 | |
147 | public function setHeight(?int $height): self |
148 | { |
149 | $this->height = $height; |
150 | return $this; |
151 | } |
152 | |
153 | public function getHeight(): ?int |
154 | { |
155 | return $this->height; |
156 | } |
157 | |
158 | public function setModified(DateTimeInterface $modified): self |
159 | { |
160 | $this->modified = $modified; |
161 | return $this; |
162 | } |
163 | |
164 | public function getModified(): DateTimeInterface |
165 | { |
166 | return $this->modified; |
167 | } |
168 | |
169 | // @codeCoverageIgnoreEnd |
170 | // }}} Autocode |
171 | |
172 | public function getMimetypeMajor(): ?string |
173 | { |
174 | $mime = $this->getMimetype(); |
175 | return is_null($mime) ? $mime : GSFile::mimetypeMajor($mime); |
176 | } |
177 | |
178 | public function getMimetypeMinor(): ?string |
179 | { |
180 | $mime = $this->getMimetype(); |
181 | return is_null($mime) ? $mime : GSFile::mimetypeMinor($mime); |
182 | } |
183 | |
184 | /** |
185 | * @return int |
186 | */ |
187 | public function livesIncrementAndGet(): int |
188 | { |
189 | ++$this->lives; |
190 | return $this->lives; |
191 | } |
192 | |
193 | /** |
194 | * @return int |
195 | */ |
196 | public function livesDecrementAndGet(): int |
197 | { |
198 | --$this->lives; |
199 | return $this->lives; |
200 | } |
201 | |
202 | const FILEHASH_ALGO = 'sha256'; |
203 | |
204 | /** |
205 | * Delete a file if safe, removes dependencies, cleanups and flushes |
206 | * |
207 | * @return bool |
208 | */ |
209 | public function kill(): bool |
210 | { |
211 | if ($this->livesDecrementAndGet() <= 0) { |
212 | return $this->delete(); |
213 | } |
214 | return true; |
215 | } |
216 | |
217 | /** |
218 | * Remove the respective file from disk |
219 | */ |
220 | public function deleteStorage(): bool |
221 | { |
222 | if (!is_null($filepath = $this->getPath())) { |
223 | if (file_exists($filepath)) { |
224 | if (@unlink($filepath) === false) { |
225 | // @codeCoverageIgnoreStart |
226 | Log::error("Failed deleting file for attachment with id={$this->getId()} at {$filepath}."); |
227 | return false; |
228 | // @codeCoverageIgnoreEnd |
229 | } else { |
230 | $this->setFilename(null); |
231 | $this->setSize(null); |
232 | // Important not to null neither width nor height |
233 | DB::persist($this); |
234 | DB::flush(); |
235 | } |
236 | } else { |
237 | // @codeCoverageIgnoreStart |
238 | Log::warning("File for attachment with id={$this->getId()} at {$filepath} was already deleted when I was going to handle it."); |
239 | // @codeCoverageIgnoreEnd |
240 | } |
241 | } |
242 | return true; |
243 | } |
244 | |
245 | /** |
246 | * Attachment delete always removes dependencies, cleanups and flushes |
247 | */ |
248 | protected function delete(): bool |
249 | { |
250 | if ($this->getLives() > 0) { |
251 | // @codeCoverageIgnoreStart |
252 | Log::warning("Deleting file {$this->getId()} with {$this->getLives()} lives. Why are you killing it so young?"); |
253 | // @codeCoverageIgnoreEnd |
254 | } |
255 | // Delete related files from storage |
256 | $files = []; |
257 | if (!is_null($filepath = $this->getPath())) { |
258 | $files[] = $filepath; |
259 | } |
260 | foreach ($this->getThumbnails() as $at) { |
261 | $files[] = $at->getPath(); |
262 | $at->delete(flush: false); |
263 | } |
264 | DB::remove($this); |
265 | foreach ($files as $f) { |
266 | if (file_exists($f)) { |
267 | if (@unlink($f) === false) { |
268 | // @codeCoverageIgnoreStart |
269 | Log::error("Failed deleting file for attachment with id={$this->getId()} at {$f}."); |
270 | // @codeCoverageIgnoreEnd |
271 | } |
272 | } else { |
273 | // @codeCoverageIgnoreStart |
274 | Log::warning("File for attachment with id={$this->getId()} at {$f} was already deleted when I was going to handle it."); |
275 | // @codeCoverageIgnoreEnd |
276 | } |
277 | } |
278 | DB::flush(); |
279 | return true; |
280 | } |
281 | |
282 | /** |
283 | * TODO: Maybe this isn't the best way of handling titles |
284 | * |
285 | * @param null|Note $note |
286 | * |
287 | * @throws DuplicateFoundException |
288 | * @throws NotFoundException |
289 | * @throws ServerException |
290 | * |
291 | * @return string |
292 | */ |
293 | public function getBestTitle(?Note $note = null): string |
294 | { |
295 | // If we have a note, then the best title is the title itself |
296 | if (!is_null(($note))) { |
297 | $attachment_to_note = DB::findOneBy('attachment_to_note', [ |
298 | 'attachment_id' => $this->getId(), |
299 | 'note_id' => $note->getId(), |
300 | ]); |
301 | if (!is_null($attachment_to_note->getTitle())) { |
302 | return $attachment_to_note->getTitle(); |
303 | } |
304 | } |
305 | // Else |
306 | if (!is_null($filename = $this->getFilename())) { |
307 | // A filename would do just as well |
308 | return $filename; |
309 | } else { |
310 | // Welp |
311 | return _m('Untitled attachment'); |
312 | } |
313 | } |
314 | |
315 | /** |
316 | * Find all thumbnails associated with this attachment. Don't bother caching as this is not supposed to be a common operation |
317 | */ |
318 | public function getThumbnails() |
319 | { |
320 | return DB::findBy('attachment_thumbnail', ['attachment_id' => $this->id]); |
321 | } |
322 | |
323 | public function getPath() |
324 | { |
325 | $filename = $this->getFilename(); |
326 | return is_null($filename) ? null : Common::config('attachments', 'dir') . DIRECTORY_SEPARATOR . $filename; |
327 | } |
328 | |
329 | public function getUrl() |
330 | { |
331 | return Router::url('attachment_view', ['id' => $this->getId()]); |
332 | } |
333 | |
334 | public function getThumbnailUrl() |
335 | { |
336 | return Router::url('attachment_thumbnail', ['id' => $this->getId(), 'w' => Common::config('thumbnail', 'width'), 'h' => Common::config('thumbnail', 'height')]); |
337 | } |
338 | |
339 | public static function schemaDef(): array |
340 | { |
341 | return [ |
342 | 'name' => 'attachment', |
343 | 'fields' => [ |
344 | 'id' => ['type' => 'serial', 'not null' => true], |
345 | 'lives' => ['type' => 'int', 'not null' => true, 'description' => 'RefCount'], |
346 | 'filehash' => ['type' => 'varchar', 'length' => 64, 'description' => 'sha256 of the file contents, if the file is stored locally'], |
347 | 'mimetype' => ['type' => 'varchar', 'length' => 255, 'description' => 'resource mime type 127+1+127 as per rfc6838#section-4.2'], |
348 | 'filename' => ['type' => 'varchar', 'length' => 191, 'description' => 'file name of resource when available'], |
349 | 'size' => ['type' => 'int', 'description' => 'size of resource when available'], |
350 | 'width' => ['type' => 'int', 'description' => 'width in pixels, if it can be described as such and data is available'], |
351 | 'height' => ['type' => 'int', 'description' => 'height in pixels, if it can be described as such and data is available'], |
352 | 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], |
353 | ], |
354 | 'primary key' => ['id'], |
355 | 'unique keys' => [ |
356 | 'attachment_filehash_uniq' => ['filehash'], |
357 | 'attachment_filename_uniq' => ['filename'], |
358 | ], |
359 | 'indexes' => [ |
360 | 'file_filehash_idx' => ['filehash'], |
361 | ], |
362 | ]; |
363 | } |
364 | } |