]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/Document.php
Propose existing types on document form, not only system ones
[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 private bool $public_list = false;
77
78 /**
79 * Main constructor
80 *
81 * @param Db $zdb Database instance
82 * @param int|ArrayObject<string,int|string>|null $args Arguments
83 */
84 public function __construct(Db $zdb, int|ArrayObject $args = null)
85 {
86 $this->zdb = $zdb;
87 $this->can_public = true;
88
89 $this->init($this->store_path);
90
91 if (is_int($args)) {
92 $this->load($args);
93 } elseif ($args instanceof ArrayObject) {
94 $this->loadFromRs($args);
95 }
96 }
97
98 /**
99 * Load a document from its identifier
100 *
101 * @param integer $id Identifier
102 *
103 * @return void
104 */
105 private function load(int $id): void
106 {
107 try {
108 $select = $this->zdb->select(self::TABLE);
109 $select->limit(1)->where([self::PK => $id]);
110
111 $results = $this->zdb->execute($select);
112 /** @var ArrayObject<string, int|string> $res */
113 $res = $results->current();
114 $this->loadFromRs($res);
115 } catch (Throwable $e) {
116 Analog::log(
117 'An error occurred loading document #' . $id . "Message:\n" .
118 $e->getMessage(),
119 Analog::ERROR
120 );
121 }
122 }
123
124 /**
125 * Get documents
126 *
127 * @param string|null $type Type to retrieve
128 *
129 * @return array<int,Document>
130 *
131 * @throws Throwable
132 */
133 public function getList(string $type = null): array
134 {
135 global $login;
136
137 try {
138 $select = $this->zdb->select(self::TABLE);
139
140 if ($type !== null) {
141 $select->where(['type' => $type]);
142 }
143
144 $select->order(self::PK);
145
146 $results = $this->zdb->execute($select);
147 $documents = [];
148 $access_level = $login->getAccessLevel();
149
150 foreach ($results as $r) {
151 // skip entries according to access control
152 if (
153 $r->visible == FieldsConfig::NOBODY &&
154 ($this->public_list === true || ($this->public_list === false && !$login->isAdmin())) ||
155 ($r->visible == FieldsConfig::ADMIN &&
156 $access_level < Authentication::ACCESS_ADMIN) ||
157 ($r->visible == FieldsConfig::STAFF &&
158 $access_level < Authentication::ACCESS_STAFF) ||
159 ($r->visible == FieldsConfig::MANAGER &&
160 $access_level < Authentication::ACCESS_MANAGER) ||
161 (($r->visible == FieldsConfig::USER_READ || $r->visible == FieldsConfig::USER_WRITE) &&
162 $access_level < Authentication::ACCESS_USER)
163 ) {
164 continue;
165 }
166
167 $documents[$r->{self::PK}] = new Document($this->zdb, $r);
168 }
169 return $documents;
170 } catch (Throwable $e) {
171 Analog::log(
172 "An error occurred loading documents. Message:\n" .
173 $e->getMessage(),
174 Analog::ERROR
175 );
176 throw $e;
177 }
178 }
179
180 /**
181 * Get list by type
182 *
183 * @return array<string, array<int, Document>>
184 *
185 * @throws Throwable
186 */
187 public function getTypedList(): array
188 {
189 $this->public_list = true;
190 $list = $this->getList();
191 $sys_types = $this->getSystemTypes(false);
192
193 $typed_list = array_fill_keys($sys_types, []);
194 foreach ($list as $doc_id => $document) {
195 $typed_list[$document->getType()][] = $document;
196 }
197
198 //cleanup: some system types may have no entries
199 foreach ($sys_types as $type) {
200 if (count($typed_list[$type]) == 0) {
201 unset($typed_list[$type]);
202 }
203 }
204
205 return $typed_list;
206 }
207
208 /**
209 * Check if a document can be shown
210 *
211 * @param Login $login Login
212 *
213 * @return boolean
214 */
215 public function canShow(Login $login): bool
216 {
217 $access_level = $login->getAccessLevel();
218
219 switch ($this->getPermission()) {
220 case FieldsConfig::ALL:
221 return true;
222 case FieldsConfig::NOBODY:
223 return false;
224 case FieldsConfig::ADMIN:
225 return $access_level >= Authentication::ACCESS_ADMIN;
226 case FieldsConfig::STAFF:
227 return $access_level >= Authentication::ACCESS_STAFF;
228 case FieldsConfig::MANAGER:
229 return $access_level >= Authentication::ACCESS_MANAGER;
230 case FieldsConfig::USER_WRITE:
231 case FieldsConfig::USER_READ:
232 return $access_level >= Authentication::ACCESS_USER;
233 }
234
235 return false;
236 }
237
238 /**
239 * Load document from a db ResultSet
240 *
241 * @param ArrayObject<string, int|string> $rs ResultSet
242 *
243 * @return void
244 */
245 private function loadFromRs(ArrayObject $rs): void
246 {
247 $this->id = $rs->{self::PK};
248 $this->type = $rs->type;
249 $this->permission = $rs->visible;
250 $this->filename = $rs->filename;
251 $this->comment = $rs->comment;
252 $this->creation_date = new DateTime($rs->creation_date);
253 }
254
255 /**
256 * Store document in database
257 *
258 * @param array<string,mixed> $post POST data
259 * @param array<string,mixed> $files Files
260 *
261 * @return boolean
262 */
263 public function store(array $post, array $files): bool
264 {
265 $this->setType($post['document_type']);
266 $this->setComment($post['comment']);
267 $this->permission = $post['visible'];
268
269 $handled = $this->handleFiles($files);
270 if ($handled !== true) {
271 $this->errors = $handled;
272 return false;
273 }
274
275 try {
276 $values = [
277 'type' => $this->type,
278 'filename' => $this->filename,
279 'visible' => $this->getPermission(),
280 'comment' => $this->comment,
281 ];
282 if (isset($this->id) && $this->id > 0) {
283 $update = $this->zdb->update(self::TABLE);
284 $update->set($values)->where([self::PK => $this->id]);
285 $this->zdb->execute($update);
286 } else {
287 $values['creation_date'] = date('Y-m-d H:i:s');
288 $insert = $this->zdb->insert(self::TABLE);
289 $insert->values($values);
290 $add = $this->zdb->execute($insert);
291 if (!$add->count() > 0) {
292 Analog::log('Not stored!', Analog::ERROR);
293 return false;
294 }
295
296 $this->id = $this->zdb->getLastGeneratedValue($this);
297 if (!in_array($this->type, $this->getSystemTypes(false))) {
298 $this->addTranslation($this->type);
299 }
300 }
301 return true;
302 } catch (Throwable $e) {
303 $this->removeFile();
304 Analog::log(
305 'An error occurred storing document: ' . $e->getMessage(),
306 Analog::ERROR
307 );
308 throw $e;
309 }
310 }
311
312 /**
313 * Remove document
314 *
315 * @param array<int>|null $ids IDs to remove, default to current id
316 *
317 * @return boolean
318 */
319 public function remove(array $ids = null): bool
320 {
321 if ($ids == null) {
322 $ids[] = $this->id;
323 }
324
325 try {
326 $this->zdb->connection->beginTransaction();
327 $delete = $this->zdb->delete(self::TABLE);
328 $delete->where([self::PK => $ids]);
329 $this->zdb->execute($delete);
330 if (!$this->removeFile()) {
331 throw new \RuntimeException('cannot remove file document from disk');
332 }
333 Analog::log(
334 'Document #' . implode(', #', $ids) . ' deleted successfully.',
335 Analog::INFO
336 );
337
338 $this->zdb->connection->commit();
339 return true;
340 } catch (Throwable $e) {
341 $this->zdb->connection->rollBack();
342 Analog::log(
343 'Unable to delete document #' . implode(', #', $ids) . ' | ' . $e->getMessage(),
344 Analog::ERROR
345 );
346 throw $e;
347 }
348 }
349
350 /**
351 * Remove document file
352 *
353 * @return bool
354 */
355 protected function removeFile(): bool
356 {
357 $file = $this->getDestDir() . $this->getDocumentFilename();
358 if (file_exists($file)) {
359 return unlink($file);
360 }
361
362 Analog::log('File ' . $file . ' does not exist', Analog::WARNING);
363 return false;
364 }
365
366 /**
367 * Get file URL
368 *
369 * @return string
370 */
371 public function getURL(): string
372 {
373 return $this->getDestDir() . $this->getDocumentFileName();
374 }
375
376 /**
377 * Get document ID
378 *
379 * @return ?int
380 */
381 public function getId(): ?int
382 {
383 return $this->id ?? null;
384 }
385
386 /**
387 * Get document file name
388 *
389 * @return string
390 */
391 public function getDocumentFilename(): string
392 {
393 return $this->filename ?? '';
394 }
395
396 /**
397 * Set comment
398 * @param ?string $comment Comment to set
399 *
400 * @return self
401 */
402 public function setComment(?string $comment): self
403 {
404 $this->comment = $comment;
405 return $this;
406 }
407
408 /**
409 * Get comment
410 *
411 * @return ?string
412 */
413 public function getComment(): ?string
414 {
415 return $this->comment;
416 }
417
418 /**
419 * Set type
420 *
421 * @param string $type Type
422 *
423 * @return self
424 */
425 public function setType(string $type): self
426 {
427 $this->type = $type;
428 return $this;
429 }
430
431 /**
432 * Get type
433 *
434 * @return string
435 */
436 public function getType(): string
437 {
438 return $this->type ?? '';
439 }
440
441 /**
442 * Get creation date
443 *
444 * @param boolean $formatted Return formatted date (default) or not
445 *
446 * @return string|DateTime
447 */
448 public function getCreationDate(bool $formatted = true): string|DateTime
449 {
450 if ($formatted) {
451 return $this->creation_date->format(_T('Y-m-d H:i:s'));
452 }
453 return $this->creation_date;
454 }
455
456 /**
457 * Get system social types
458 *
459 * @param boolean $translated Return translated types (default) or not
460 *
461 * @return array<string,string>
462 */
463 public function getSystemTypes(bool $translated = true): array
464 {
465 if ($translated) {
466 $systypes = [
467 self::STATUS => _T('Association status'),
468 self::RULES => _T('Rules of procedure'),
469 self::ADHESION => _T('Adhesion form'),
470 self::MINUTES => _T('Meeting minutes'),
471 self::VOTES => _T('Votes results')
472 ];
473 } else {
474 $systypes = [
475 self::STATUS => 'Association status',
476 self::RULES => 'Rules of procedure',
477 self::ADHESION => 'Adhesion form',
478 self::MINUTES => 'Meeting minutes',
479 self::VOTES => 'Votes results'
480 ];
481 }
482 return $systypes;
483 }
484
485 /**
486 * Get system documents types
487 *
488 * @param string $type Document type
489 * @param boolean $translated Return translated types (default) or not
490 *
491 * @return string
492 */
493 public function getSystemType(string $type, bool $translated = true): string
494 {
495 return $this->getSystemTypes($translated)[$type] ?? _T($type);
496 }
497
498 /**
499 * Get all known types
500 *
501 * @return array<string,string>
502 *
503 * @throws Throwable
504 */
505 public function getTypes(): array
506 {
507 $types = $this->getSystemTypes();
508
509 $select = $this->zdb->select(self::TABLE);
510 $select->quantifier('DISTINCT');
511 $select->where->notIn('type', array_keys($this->getSystemTypes(false)));
512 $results = $this->zdb->execute($select);
513
514 foreach ($results as $r) {
515 $types[$r->type] = $r->type;
516 }
517
518 return $types;
519 }
520
521 /**
522 * Handle files
523 *
524 * @param array<string,mixed> $files Files sent
525 *
526 * @return array<string>|true
527 */
528 public function handleFiles(array $files): array|bool
529 {
530 $this->errors = [];
531 // document upload
532 if (isset($files['document_file'])) {
533 if ($files['document_file']['error'] === UPLOAD_ERR_OK) {
534 if ($files['document_file']['tmp_name'] != '') {
535 if (is_uploaded_file($files['document_file']['tmp_name'])) {
536 $res = $this->trait_store($files['document_file']);
537 if ($res < 0) {
538 $this->errors[] = $this->getErrorMessage($res);
539 } else {
540 $this->filename = sprintf(
541 '%s.%s',
542 $this->name_wo_ext,
543 $this->extension
544 );
545 }
546 }
547 }
548 } elseif (!isset($this->id)) {
549 Analog::log(
550 $this->getPhpErrorMessage($files['document_file']['error']),
551 Analog::WARNING
552 );
553 $this->errors[] = $this->getPhpErrorMessage($files['document_file']['error']);
554 }
555 }
556
557 if (count($this->errors) > 0) {
558 Analog::log(
559 'Some errors has been thew attempting to edit/store a document file' . "\n" .
560 print_r($this->errors, true),
561 Analog::ERROR
562 );
563 return $this->errors;
564 } else {
565 return true;
566 }
567 }
568
569 /**
570 * Get errors
571 *
572 * @return string[]
573 */
574 public function getErrors(): array
575 {
576 return $this->errors;
577 }
578
579 /**
580 * Write file on disk
581 *
582 * @param string $tmpfile Temporary file
583 * @param bool $ajax If the file comes from an ajax call (dnd)
584 *
585 * @return bool|int
586 */
587 public function writeOnDisk(string $tmpfile, bool $ajax): bool|int
588 {
589 //remove existing file when updating
590 if (isset($this->id) && $this->id > 0) {
591 $this->removeFile();
592 }
593 return $this->trait_writeOnDisk($tmpfile, $ajax);
594 }
595 }