]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/Document.php
Add association's documents management
[galette.git] / galette / lib / Galette / Entity / Document.php
1 <?php
2
3 /**
4 * Copyright © 2003-2024 The Galette Team
5 *
6 * This file is part of Galette (https://galette.eu).
7 *
8 * Galette is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * Galette is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22 namespace Galette\Entity;
23
24 use ArrayObject;
25 use DateTime;
26 use Galette\Core\Authentication;
27 use Galette\Core\Login;
28 use Galette\Features\I18n;
29 use Galette\Features\Permissions;
30 use Galette\IO\FileInterface;
31 use Galette\IO\FileTrait;
32 use Throwable;
33 use Galette\Core\Db;
34 use Analog\Analog;
35
36 /**
37 * Documents
38 *
39 * @author Johan Cwiklinski <johan@x-tnd.be>
40 */
41
42 class Document implements FileInterface
43 {
44 use I18n;
45 use Permissions;
46 use FileTrait {
47 store as protected trait_store;
48 writeOnDisk as protected trait_writeOnDisk;
49 }
50
51 public const TABLE = 'documents';
52 public const PK = 'id_document';
53
54 public const STATUS = 'status';
55 public const RULES = 'rules';
56 public const ADHESION = 'adhesion';
57 public const MINUTES = 'minutes';
58 public const VOTES = 'votes';
59
60 /** @var Db */
61 private Db $zdb;
62 /** @var int */
63 private int $id;
64 /** @var string */
65 private string $type;
66 /** @var string */
67 private string $filename;
68 /** @var DateTime */
69 private DateTime $creation_date;
70 /** @var string */
71 protected string $store_path = GALETTE_DOCUMENTS_PATH;
72 /** @var ?string */
73 private ?string $comment = null;
74 /** @var array<string> */
75 private array $errors = [];
76
77 /**
78 * Main constructor
79 *
80 * @param Db $zdb Database instance
81 * @param int|ArrayObject<string,int|string>|null $args Arguments
82 */
83 public function __construct(Db $zdb, int|ArrayObject $args = null)
84 {
85 $this->zdb = $zdb;
86 $this->can_public = true;
87
88 $this->init($this->store_path);
89
90 if (is_int($args)) {
91 $this->load($args);
92 } elseif ($args instanceof ArrayObject) {
93 $this->loadFromRs($args);
94 }
95 }
96
97 /**
98 * Load a document from its identifier
99 *
100 * @param integer $id Identifier
101 *
102 * @return void
103 */
104 private function load(int $id): void
105 {
106 try {
107 $select = $this->zdb->select(self::TABLE);
108 $select->limit(1)->where([self::PK => $id]);
109
110 $results = $this->zdb->execute($select);
111 /** @var ArrayObject<string, int|string> $res */
112 $res = $results->current();
113 $this->loadFromRs($res);
114 } catch (Throwable $e) {
115 Analog::log(
116 'An error occurred loading document #' . $id . "Message:\n" .
117 $e->getMessage(),
118 Analog::ERROR
119 );
120 }
121 }
122
123 /**
124 * Get documents
125 *
126 * @param string|null $type Type to retrieve
127 *
128 * @return array<int,Document>
129 *
130 * @throws Throwable
131 */
132 public static function getList(string $type = null): array
133 {
134 global $zdb, $login;
135
136 try {
137 $select = $zdb->select(self::TABLE);
138
139 if ($type !== null) {
140 $select->where(['type' => $type]);
141 }
142
143 $select->order(self::PK);
144
145 $results = $zdb->execute($select);
146 $documents = [];
147 $access_level = $login->getAccessLevel();
148
149 foreach ($results as $r) {
150 // skip entries according to access control
151 if (
152 $r->visible == FieldsConfig::NOBODY ||
153 ($r->visible == FieldsConfig::ADMIN &&
154 $access_level < Authentication::ACCESS_ADMIN) ||
155 ($r->visible == FieldsConfig::STAFF &&
156 $access_level < Authentication::ACCESS_STAFF) ||
157 ($r->visible == FieldsConfig::MANAGER &&
158 $access_level < Authentication::ACCESS_MANAGER) ||
159 (($r->visible == FieldsConfig::USER_READ || $r->visible == FieldsConfig::USER_WRITE) &&
160 $access_level < Authentication::ACCESS_USER)
161 ) {
162 continue;
163 }
164
165 $documents[$r->{self::PK}] = new Document($zdb, $r);
166 }
167 return $documents;
168 } catch (Throwable $e) {
169 Analog::log(
170 "An error occurred loading documents. Message:\n" .
171 $e->getMessage(),
172 Analog::ERROR
173 );
174 throw $e;
175 }
176 }
177
178 /**
179 * Get list by type
180 *
181 * @return array<string, array<int, Document>>
182 *
183 * @throws Throwable
184 */
185 public function getTypedList(): array
186 {
187 $list = $this->getList();
188 $sys_types = $this->getSystemTypes(false);
189
190 $typed_list = array_fill_keys($sys_types, []);
191 foreach ($list as $doc_id => $document) {
192 $typed_list[$document->getType()][] = $document;
193 }
194
195 //cleanup: some system types may have no entries
196 foreach ($sys_types as $type) {
197 if (count($typed_list[$type]) == 0) {
198 unset($typed_list[$type]);
199 }
200 }
201
202 return $typed_list;
203 }
204
205 /**
206 * Check if a document can be shown
207 *
208 * @param Login $login Login
209 *
210 * @return boolean
211 */
212 public function canShow(Login $login): bool
213 {
214 $access_level = $login->getAccessLevel();
215
216 switch ($this->getPermission()) {
217 case FieldsConfig::ALL:
218 return true;
219 case FieldsConfig::NOBODY:
220 return false;
221 case FieldsConfig::ADMIN:
222 return $access_level >= Authentication::ACCESS_ADMIN;
223 case FieldsConfig::STAFF:
224 return $access_level >= Authentication::ACCESS_STAFF;
225 case FieldsConfig::MANAGER:
226 return $access_level >= Authentication::ACCESS_MANAGER;
227 case FieldsConfig::USER_WRITE:
228 case FieldsConfig::USER_READ:
229 return $access_level >= Authentication::ACCESS_USER;
230 }
231
232 return false;
233 }
234
235 /**
236 * Load document from a db ResultSet
237 *
238 * @param ArrayObject<string, int|string> $rs ResultSet
239 *
240 * @return void
241 */
242 private function loadFromRs(ArrayObject $rs): void
243 {
244 $this->id = $rs->{self::PK};
245 $this->type = $rs->type;
246 $this->permission = $rs->visible;
247 $this->filename = $rs->filename;
248 $this->comment = $rs->comment;
249 $this->creation_date = new DateTime($rs->creation_date);
250 }
251
252 /**
253 * Store document in database
254 *
255 * @param array<string,mixed> $post POST data
256 * @param array<string,mixed> $files Files
257 *
258 * @return boolean
259 */
260 public function store(array $post, array $files): bool
261 {
262 $this->setType($post['document_type']);
263 $this->setComment($post['comment']);
264 $this->permission = $post['visible'];
265
266 $handled = $this->handleFiles($files);
267 if ($handled !== true) {
268 $this->errors = $handled;
269 return false;
270 }
271
272 try {
273 $values = [
274 'type' => $this->type,
275 'filename' => $this->filename,
276 'visible' => $this->getPermission(),
277 'comment' => $this->comment,
278 ];
279 if (isset($this->id) && $this->id > 0) {
280 $update = $this->zdb->update(self::TABLE);
281 $update->set($values)->where([self::PK => $this->id]);
282 $this->zdb->execute($update);
283 } else {
284 $values['creation_date'] = date('Y-m-d H:i:s');
285 $insert = $this->zdb->insert(self::TABLE);
286 $insert->values($values);
287 $add = $this->zdb->execute($insert);
288 if (!$add->count() > 0) {
289 Analog::log('Not stored!', Analog::ERROR);
290 return false;
291 }
292
293 $this->id = $this->zdb->getLastGeneratedValue($this);
294 if (!in_array($this->type, $this->getSystemTypes(false))) {
295 $this->addTranslation($this->type);
296 }
297 }
298 return true;
299 } catch (Throwable $e) {
300 $this->removeFile();
301 Analog::log(
302 'An error occurred storing document: ' . $e->getMessage(),
303 Analog::ERROR
304 );
305 throw $e;
306 }
307 }
308
309 /**
310 * Remove document
311 *
312 * @param array<int>|null $ids IDs to remove, default to current id
313 *
314 * @return boolean
315 */
316 public function remove(array $ids = null): bool
317 {
318 if ($ids == null) {
319 $ids[] = $this->id;
320 }
321
322 try {
323 $this->zdb->connection->beginTransaction();
324 $delete = $this->zdb->delete(self::TABLE);
325 $delete->where([self::PK => $ids]);
326 $this->zdb->execute($delete);
327 if (!$this->removeFile()) {
328 throw new \RuntimeException('cannot remove file document from disk');
329 }
330 Analog::log(
331 'Document #' . implode(', #', $ids) . ' deleted successfully.',
332 Analog::INFO
333 );
334
335 $this->zdb->connection->commit();
336 return true;
337 } catch (Throwable $e) {
338 $this->zdb->connection->rollBack();
339 Analog::log(
340 'Unable to delete document #' . implode(', #', $ids) . ' | ' . $e->getMessage(),
341 Analog::ERROR
342 );
343 throw $e;
344 }
345 }
346
347 /**
348 * Remove document file
349 *
350 * @return bool
351 */
352 protected function removeFile(): bool
353 {
354 $file = $this->getDestDir() . $this->getDocumentFilename();
355 if (file_exists($file)) {
356 return unlink($file);
357 }
358
359 Analog::log('File ' . $file . ' does not exist', Analog::WARNING);
360 return false;
361 }
362
363 /**
364 * Get file URL
365 *
366 * @return string
367 */
368 public function getURL(): string
369 {
370 return $this->getDestDir() . $this->getDocumentFileName();
371 }
372
373 /**
374 * Get document ID
375 *
376 * @return ?int
377 */
378 public function getId(): ?int
379 {
380 return $this->id ?? null;
381 }
382
383 /**
384 * Get document file name
385 *
386 * @return string
387 */
388 public function getDocumentFilename(): string
389 {
390 return $this->filename ?? '';
391 }
392
393 /**
394 * Set comment
395 * @param ?string $comment Comment to set
396 *
397 * @return self
398 */
399 public function setComment(?string $comment): self
400 {
401 $this->comment = $comment;
402 return $this;
403 }
404
405 /**
406 * Get comment
407 *
408 * @return ?string
409 */
410 public function getComment(): ?string
411 {
412 return $this->comment;
413 }
414
415 /**
416 * Set type
417 *
418 * @param string $type Type
419 *
420 * @return self
421 */
422 public function setType(string $type): self
423 {
424 $this->type = $type;
425 return $this;
426 }
427
428 /**
429 * Get type
430 *
431 * @return string
432 */
433 public function getType(): string
434 {
435 return $this->type ?? '';
436 }
437
438 /**
439 * Get creation date
440 *
441 * @param boolean $formatted Return formatted date (default) or not
442 *
443 * @return string|DateTime
444 */
445 public function getCreationDate(bool $formatted = true): string|DateTime
446 {
447 if ($formatted) {
448 return $this->creation_date->format(_T('Y-m-d H:i:s'));
449 }
450 return $this->creation_date;
451 }
452
453 /**
454 * Get system social types
455 *
456 * @param boolean $translated Return translated types (default) or not
457 *
458 * @return array<string,string>
459 */
460 public function getSystemTypes(bool $translated = true): array
461 {
462 if ($translated) {
463 $systypes = [
464 self::STATUS => _T('Association status'),
465 self::RULES => _T('Rules of procedure'),
466 self::ADHESION => _T('Adhesion form'),
467 self::MINUTES => _T('Meeting minutes'),
468 self::VOTES => _T('Votes results')
469 ];
470 } else {
471 $systypes = [
472 self::STATUS => 'Association status',
473 self::RULES => 'Rules of procedure',
474 self::ADHESION => 'Adhesion form',
475 self::MINUTES => 'Meeting minutes',
476 self::VOTES => 'Votes results'
477 ];
478 }
479 return $systypes;
480 }
481
482 /**
483 * Get system documents types
484 *
485 * @param string $type Document type
486 * @param boolean $translated Return translated types (default) or not
487 *
488 * @return string
489 */
490 public function getSystemType(string $type, bool $translated = true): string
491 {
492 return $this->getSystemTypes($translated)[$type] ?? _T($type);
493 }
494
495 /**
496 * Handle files
497 *
498 * @param array<string,mixed> $files Files sent
499 *
500 * @return array<string>|true
501 */
502 public function handleFiles(array $files): array|bool
503 {
504 $this->errors = [];
505 // document upload
506 if (isset($files['document_file'])) {
507 if ($files['document_file']['error'] === UPLOAD_ERR_OK) {
508 if ($files['document_file']['tmp_name'] != '') {
509 if (is_uploaded_file($files['document_file']['tmp_name'])) {
510 $res = $this->trait_store($files['document_file']);
511 if ($res < 0) {
512 $this->errors[] = $this->getErrorMessage($res);
513 } else {
514 $this->filename = sprintf(
515 '%s.%s',
516 $this->name_wo_ext,
517 $this->extension
518 );
519 }
520 }
521 }
522 } elseif (!isset($this->id)) {
523 Analog::log(
524 $this->getPhpErrorMessage($files['document_file']['error']),
525 Analog::WARNING
526 );
527 $this->errors[] = $this->getPhpErrorMessage($files['document_file']['error']);
528 }
529 }
530
531 if (count($this->errors) > 0) {
532 Analog::log(
533 'Some errors has been thew attempting to edit/store a document file' . "\n" .
534 print_r($this->errors, true),
535 Analog::ERROR
536 );
537 return $this->errors;
538 } else {
539 return true;
540 }
541 }
542
543 /**
544 * Get errors
545 *
546 * @return string[]
547 */
548 public function getErrors(): array
549 {
550 return $this->errors;
551 }
552
553 /**
554 * Write file on disk
555 *
556 * @param string $tmpfile Temporary file
557 * @param bool $ajax If the file comes from an ajax call (dnd)
558 *
559 * @return bool|int
560 */
561 public function writeOnDisk(string $tmpfile, bool $ajax): bool|int
562 {
563 //remove existing file when updating
564 if (isset($this->id) && $this->id > 0) {
565 $this->removeFile();
566 }
567 return $this->trait_writeOnDisk($tmpfile, $ajax);
568 }
569 }