]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/Adherent.php
Resize and crop members picture to a fixed ratio
[galette.git] / galette / lib / Galette / Entity / Adherent.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Member class for galette
7 *
8 * PHP version 5
9 *
10 * Copyright © 2009-2023 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
18 *
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
26 *
27 * @category Entity
28 * @package Galette
29 *
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2009-2023 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.7dev - 2009-06-02
35 */
36
37 namespace Galette\Entity;
38
39 use ArrayObject;
40 use Galette\Events\GaletteEvent;
41 use Galette\Features\Socials;
42 use Throwable;
43 use Analog\Analog;
44 use Laminas\Db\Sql\Expression;
45 use Galette\Core\Db;
46 use Galette\Core\Picture;
47 use Galette\Core\GaletteMail;
48 use Galette\Core\Password;
49 use Galette\Core\Preferences;
50 use Galette\Core\History;
51 use Galette\Repository\Groups;
52 use Galette\Core\Login;
53 use Galette\Repository\Members;
54 use Galette\Features\Dynamics;
55
56 /**
57 * Member class for galette
58 *
59 * @category Entity
60 * @name Adherent
61 * @package Galette
62 * @author Johan Cwiklinski <johan@x-tnd.be>
63 * @copyright 2009-2023 The Galette Team
64 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
65 * @link http://galette.tuxfamily.org
66 * @since Available since 0.7dev - 02-06-2009
67 *
68 * @property integer $id
69 * @property integer|Title $title Either a title id or an instance of Title
70 * @property string $stitle Title label
71 * @property string $company_name
72 * @property string $name
73 * @property string $surname
74 * @property string $nickname
75 * @property string $birthdate Localized birthdate
76 * @property string $rbirthdate Raw birthdate
77 * @property string $birth_place
78 * @property integer $gender
79 * @property string $sgender Gender label
80 * @property string $job
81 * @property string $language
82 * @property integer $status
83 * @property string $sstatus Status label
84 * @property string $address
85 * @property string $zipcode
86 * @property string $town
87 * @property string $country
88 * @property string $phone
89 * @property string $gsm
90 * @property string $email
91 * @property string $gnupgid
92 * @property string $fingerprint
93 * @property string $login
94 * @property string $creation_date Localized creation date
95 * @property string $modification_date Localized modification date
96 * @property string $due_date Localized due date
97 * @property string $others_infos
98 * @property string $others_infos_admin
99 * @property Picture $picture
100 * @property array $groups
101 * @property array $managed_groups
102 * @property integer|Adherent $parent Parent id if parent dep is not loaded, Adherent instance otherwise
103 * @property array $children
104 * @property boolean $admin better to rely on isAdmin()
105 * @property boolean $staff better to rely on isStaff()
106 * @property boolean $due_free better to rely on isDueFree()
107 * @property boolean $appears_in_list better to rely on appearsInMembersList()
108 * @property boolean $active better to rely on isActive()
109 * @property boolean $duplicate better to rely on isDuplicate()
110 * @property string $sadmin yes/no
111 * @property string $sstaff yes/no
112 * @property string $sdue_free yes/no
113 * @property string $sappears_in_list yes/no
114 * @property string $sactive yes/no
115 * @property string $sfullname
116 * @property string $sname
117 * @property string $saddress
118 * @property string $contribstatus State of member contributions
119 * @property integer $days_remaining
120 * @property-read integer $parent_id
121 * @property Social $social Social networks/Contact
122 * @property string $number Member number
123 * @property-read bool $self_adh
124 */
125 class Adherent
126 {
127 use Dynamics;
128 use Socials;
129
130 public const TABLE = 'adherents';
131 public const PK = 'id_adh';
132
133 public const NC = 0;
134 public const MAN = 1;
135 public const WOMAN = 2;
136
137 public const AFTER_ADD_DEFAULT = 0;
138 public const AFTER_ADD_TRANS = 1;
139 public const AFTER_ADD_NEW = 2;
140 public const AFTER_ADD_SHOW = 3;
141 public const AFTER_ADD_LIST = 4;
142 public const AFTER_ADD_HOME = 5;
143
144 private $_id;
145 //Identity
146 private $_title;
147 private $_company_name;
148 private $_name;
149 private $_surname;
150 private $_nickname;
151 private $_birthdate;
152 private $_birth_place;
153 private $_gender;
154 private $_job;
155 private $_language;
156 private $_active;
157 private $_status;
158 //Contact information
159 private $_address;
160 private $_zipcode;
161 private $_town;
162 private $_country;
163 private $_phone;
164 private $_gsm;
165 private $_email;
166 private $_gnupgid;
167 private $_fingerprint;
168 //Galette relative information
169 private $_appears_in_list;
170 private $_admin;
171 private $_staff = false;
172 private $_due_free;
173 private $_login;
174 private $_password;
175 private $_creation_date;
176 private $_modification_date;
177 private $_due_date;
178 private $_others_infos;
179 private $_others_infos_admin;
180 private $_picture;
181 private $_oldness;
182 private $_days_remaining;
183 private $_groups = [];
184 private $_managed_groups = [];
185 private $_parent;
186 private $_children = [];
187 private $_duplicate = false;
188 private $_socials;
189 private $_number;
190
191 private $_row_classes;
192
193 private $_self_adh = false;
194 protected array $_deps = array(
195 'picture' => true,
196 'groups' => true,
197 'dues' => true,
198 'parent' => false,
199 'children' => false,
200 'dynamics' => false,
201 'socials' => false
202 );
203
204 private $zdb;
205 private $preferences;
206 private $fields;
207 private $history;
208
209 private $parent_fields = [
210 'adresse_adh',
211 'cp_adh',
212 'ville_adh',
213 'email_adh'
214 ];
215
216 private $errors = [];
217
218 private $sendmail = false;
219
220 /**
221 * Default constructor
222 *
223 * @param Db $zdb Database instance
224 * @param mixed $args Either a ResultSet row, its id or its
225 * login or its email for to load s specific
226 * member, or null to just instantiate object
227 * @param false|array|null $deps Dependencies configuration, see Adherent::$_deps
228 */
229 public function __construct(Db $zdb, $args = null, $deps = null)
230 {
231 global $i18n;
232
233 $this->zdb = $zdb;
234
235 if ($deps !== null) {
236 if (is_array($deps)) {
237 $this->_deps = array_merge(
238 $this->_deps,
239 $deps
240 );
241 } elseif ($deps === false) {
242 //no dependencies
243 $this->_deps = array_fill_keys(
244 array_keys($this->_deps),
245 false
246 );
247 } else {
248 Analog::log(
249 '$deps should be an array, ' . gettype($deps) . ' given!',
250 Analog::WARNING
251 );
252 }
253 }
254
255 if ($args == null || is_int($args)) {
256 if (is_int($args) && $args > 0) {
257 $this->load($args);
258 } else {
259 $this->_active = true;
260 $this->_language = $i18n->getID();
261 $this->_creation_date = date("Y-m-d");
262 $this->_status = $this->getDefaultStatus();
263 $this->_title = null;
264 $this->_gender = self::NC;
265 $gp = new Password($this->zdb);
266 $this->_password = $gp->makeRandomPassword();
267 $this->_picture = new Picture();
268 $this->_admin = false;
269 $this->_staff = false;
270 $this->_due_free = false;
271 $this->_appears_in_list = false;
272 $this->_parent = null;
273
274 if ($this->_deps['dynamics'] === true) {
275 $this->loadDynamicFields();
276 }
277 }
278 } elseif (is_object($args)) {
279 $this->loadFromRS($args);
280 } elseif (is_string($args)) {
281 $this->loadFromLoginOrMail($args);
282 }
283 }
284
285 /**
286 * Loads a member from its id
287 *
288 * @param int $id the identifier for the member to load
289 *
290 * @return bool true if query succeed, false otherwise
291 */
292 public function load(int $id): bool
293 {
294 try {
295 $select = $this->zdb->select(self::TABLE, 'a');
296
297 $select->join(
298 array('b' => PREFIX_DB . Status::TABLE),
299 'a.' . Status::PK . '=b.' . Status::PK,
300 array('priorite_statut')
301 )->where(array(self::PK => $id));
302
303 $results = $this->zdb->execute($select);
304
305 if ($results->count() === 0) {
306 return false;
307 }
308
309 /** @var ArrayObject $result */
310 $result = $results->current();
311 $this->loadFromRS($result);
312 return true;
313 } catch (Throwable $e) {
314 Analog::log(
315 'Cannot load member form id `' . $id . '` | ' . $e->getMessage(),
316 Analog::WARNING
317 );
318 throw $e;
319 }
320 }
321
322 /**
323 * Loads a member from its login
324 *
325 * @param string $login login for the member to load
326 *
327 * @return boolean
328 */
329 public function loadFromLoginOrMail(string $login): bool
330 {
331 try {
332 $select = $this->zdb->select(self::TABLE);
333 if (GaletteMail::isValidEmail($login)) {
334 //we got a valid email address, use it
335 $select->where(array('email_adh' => $login));
336 } else {
337 ///we did not get an email address, consider using login
338 $select->where(array('login_adh' => $login));
339 }
340
341 $results = $this->zdb->execute($select);
342 if ($results->count() > 0) {
343 /** @var ArrayObject $result */
344 $result = $results->current();
345 $this->loadFromRS($result);
346 }
347 return true;
348 } catch (Throwable $e) {
349 Analog::log(
350 'Cannot load member form login `' . $login . '` | ' .
351 $e->getMessage(),
352 Analog::WARNING
353 );
354 throw $e;
355 }
356 }
357
358 /**
359 * Populate object from a resultset row
360 *
361 * @param ArrayObject $r the resultset row
362 *
363 * @return void
364 */
365 private function loadFromRS(ArrayObject $r): void
366 {
367 $this->_self_adh = false;
368 $this->_id = $r->id_adh;
369 //Identity
370 if ($r->titre_adh !== null) {
371 $this->_title = new Title((int)$r->titre_adh);
372 }
373 $this->_company_name = $r->societe_adh;
374 $this->_name = $r->nom_adh;
375 $this->_surname = $r->prenom_adh;
376 $this->_nickname = $r->pseudo_adh;
377 if ($r->ddn_adh != '1901-01-01') {
378 $this->_birthdate = $r->ddn_adh;
379 }
380 $this->_birth_place = $r->lieu_naissance;
381 $this->_gender = (int)$r->sexe_adh;
382 $this->_job = $r->prof_adh;
383 $this->_language = $r->pref_lang;
384 $this->_active = $r->activite_adh == 1;
385 $this->_status = (int)$r->id_statut;
386 //Contact information
387 $this->_address = $r->adresse_adh;
388 $this->_zipcode = $r->cp_adh;
389 $this->_town = $r->ville_adh;
390 $this->_country = $r->pays_adh;
391 $this->_phone = $r->tel_adh;
392 $this->_gsm = $r->gsm_adh;
393 $this->_email = $r->email_adh;
394 $this->_gnupgid = $r->gpgid;
395 $this->_fingerprint = $r->fingerprint;
396 //Galette relative information
397 $this->_appears_in_list = $r->bool_display_info == 1;
398 $this->_admin = $r->bool_admin_adh == 1;
399 if (
400 isset($r->priorite_statut)
401 && $r->priorite_statut < Members::NON_STAFF_MEMBERS
402 ) {
403 $this->_staff = true;
404 }
405 $this->_due_free = $r->bool_exempt_adh == 1;
406 $this->_login = $r->login_adh;
407 $this->_password = $r->mdp_adh;
408 $this->_creation_date = $r->date_crea_adh;
409 if ($r->date_modif_adh != '1901-01-01') {
410 $this->_modification_date = $r->date_modif_adh;
411 } else {
412 $this->_modification_date = $this->_creation_date;
413 }
414 $this->_due_date = $r->date_echeance;
415 $this->_others_infos = $r->info_public_adh;
416 $this->_others_infos_admin = $r->info_adh;
417 $this->_number = $r->num_adh;
418
419 if ($r->parent_id !== null) {
420 $this->_parent = (int)$r->parent_id;
421 if ($this->_deps['parent'] === true) {
422 $this->loadParent();
423 }
424 }
425
426 if ($this->_deps['children'] === true) {
427 $this->loadChildren();
428 }
429
430 if ($this->_deps['picture'] === true) {
431 $this->_picture = new Picture($this->_id);
432 }
433
434 if ($this->_deps['groups'] === true) {
435 $this->loadGroups();
436 }
437
438 if ($this->_deps['dues'] === true) {
439 $this->checkDues();
440 }
441
442 if ($this->_deps['dynamics'] === true) {
443 $this->loadDynamicFields();
444 }
445
446 if ($this->_deps['socials'] === true) {
447 $this->loadSocials();
448 }
449 }
450
451 /**
452 * Load member parent
453 *
454 * @return void
455 */
456 private function loadParent(): void
457 {
458 if ($this->_parent !== null && !$this->_parent instanceof Adherent) {
459 $deps = array_fill_keys(array_keys($this->_deps), false);
460 $this->_parent = new Adherent($this->zdb, (int)$this->_parent, $deps);
461 }
462 }
463
464 /**
465 * Load member children
466 *
467 * @return void
468 */
469 private function loadChildren(): void
470 {
471 $this->_children = array();
472 try {
473 $id = self::PK;
474 $select = $this->zdb->select(self::TABLE);
475 $select->columns(
476 array($id)
477 )->where(['parent_id' => $this->_id]);
478
479 $results = $this->zdb->execute($select);
480
481 if ($results->count() > 0) {
482 foreach ($results as $row) {
483 $deps = $this->_deps;
484 $deps['children'] = false;
485 $deps['parent'] = false;
486 $this->_children[] = new Adherent($this->zdb, (int)$row->$id, $deps);
487 }
488 }
489 } catch (Throwable $e) {
490 Analog::log(
491 'Cannot load children for member #' . $this->_id . ' | ' .
492 $e->getMessage(),
493 Analog::WARNING
494 );
495 throw $e;
496 }
497 }
498
499 /**
500 * Load member groups
501 *
502 * @return void
503 */
504 public function loadGroups(): void
505 {
506 $this->_groups = Groups::loadGroups($this->_id);
507 $this->_managed_groups = Groups::loadManagedGroups($this->_id);
508 }
509
510 /**
511 * Load member social network/contact information
512 *
513 * @return void
514 */
515 public function loadSocials(): void
516 {
517 $this->_socials = Social::getListForMember($this->_id);
518 }
519
520 /**
521 * Retrieve status from preferences
522 *
523 * @return integer
524 *
525 */
526 private function getDefaultStatus(): int
527 {
528 global $preferences;
529 if ($preferences->pref_statut != '') {
530 return $preferences->pref_statut;
531 } else {
532 Analog::log(
533 'Unable to get pref_statut; is it defined in preferences?',
534 Analog::ERROR
535 );
536 return Status::DEFAULT_STATUS;
537 }
538 }
539
540 /**
541 * Check for dues status
542 *
543 * @return void
544 */
545 private function checkDues(): void
546 {
547 //how many days since our beloved member has been created
548 $now = new \DateTime();
549 $this->_oldness = $now->diff(
550 new \DateTime($this->_creation_date)
551 )->days;
552
553 if ($this->isDueFree()) {
554 //no fee required, we don't care about dates
555 $this->_row_classes .= ' cotis-exempt';
556 } else {
557 //ok, fee is required. Let's check the dates
558 if ($this->_due_date == '') {
559 $this->_row_classes .= ' cotis-never';
560 } else {
561 // To count the days remaining, the next begin date is required.
562 $due_date = new \DateTime($this->_due_date);
563 $next_begin_date = clone $due_date;
564 $next_begin_date->add(new \DateInterval('P1D'));
565 $date_diff = $now->diff($next_begin_date);
566 $this->_days_remaining = $date_diff->days;
567 // Active
568 if ($date_diff->invert == 0 && $date_diff->days >= 0) {
569 $this->_days_remaining = $date_diff->days;
570 if ($this->_days_remaining <= 30) {
571 if ($date_diff->days == 0) {
572 $this->_row_classes .= ' cotis-lastday';
573 }
574 $this->_row_classes .= ' cotis-soon';
575 } else {
576 $this->_row_classes .= ' cotis-ok';
577 }
578 // Expired
579 } elseif ($date_diff->invert == 1 && $date_diff->days >= 0) {
580 $this->_days_remaining = $date_diff->days;
581 //check if member is still active
582 $this->_row_classes .= $this->isActive() ? ' cotis-late' : ' cotis-old';
583 }
584 }
585 }
586 }
587
588 /**
589 * Is member admin?
590 *
591 * @return bool
592 */
593 public function isAdmin(): bool
594 {
595 return $this->_admin;
596 }
597
598 /**
599 * Is user member of staff?
600 *
601 * @return bool
602 */
603 public function isStaff(): bool
604 {
605 return $this->_staff;
606 }
607
608 /**
609 * Is member freed of dues?
610 *
611 * @return bool
612 */
613 public function isDueFree(): bool
614 {
615 return $this->_due_free;
616 }
617
618 /**
619 * Is member in specified group?
620 *
621 * @param string $group_name Group name
622 *
623 * @return boolean
624 */
625 public function isGroupMember(string $group_name): bool
626 {
627 if (!$this->isDepEnabled('groups')) {
628 $this->loadGroups();
629 }
630
631 foreach ($this->_groups as $g) {
632 if ($g->getName() == $group_name) {
633 return true;
634 }
635 }
636 return false;
637 }
638
639 /**
640 * Is member manager of specified group?
641 *
642 * @param string $group_name Group name
643 *
644 * @return boolean
645 */
646 public function isGroupManager(string $group_name): bool
647 {
648 if (!$this->isDepEnabled('groups')) {
649 $this->loadGroups();
650 }
651
652 foreach ($this->_managed_groups as $mg) {
653 if ($mg->getName() == $group_name) {
654 return true;
655 }
656 }
657 return false;
658 }
659
660 /**
661 * Does current member represents a company?
662 *
663 * @return boolean
664 */
665 public function isCompany(): bool
666 {
667 return trim($this->_company_name ?? '') != '';
668 }
669
670 /**
671 * Is current member a man?
672 *
673 * @return boolean
674 */
675 public function isMan(): bool
676 {
677 return (int)$this->_gender === self::MAN;
678 }
679
680 /**
681 * Is current member a woman?
682 *
683 * @return boolean
684 */
685 public function isWoman(): bool
686 {
687 return (int)$this->_gender === self::WOMAN;
688 }
689
690
691 /**
692 * Can member appears in public members list?
693 *
694 * @return bool
695 */
696 public function appearsInMembersList(): bool
697 {
698 return $this->_appears_in_list;
699 }
700
701 /**
702 * Is member active?
703 *
704 * @return bool
705 */
706 public function isActive(): bool
707 {
708 return $this->_active;
709 }
710
711 /**
712 * Does member have uploaded a picture?
713 *
714 * @return bool
715 */
716 public function hasPicture(): bool
717 {
718 return $this->_picture->hasPicture();
719 }
720
721 /**
722 * Does member have a parent?
723 *
724 * @return bool
725 */
726 public function hasParent(): bool
727 {
728 return !empty($this->_parent);
729 }
730
731 /**
732 * Does member have children?
733 *
734 * @return bool
735 */
736 public function hasChildren(): bool
737 {
738 if ($this->_children === null) {
739 if ($this->id) {
740 Analog::log(
741 'Children has not been loaded!',
742 Analog::WARNING
743 );
744 }
745 return false;
746 } else {
747 return count($this->_children) > 0;
748 }
749 }
750
751 /**
752 * Get row class related to current fee status
753 *
754 * @param boolean $public we want the class for public pages
755 *
756 * @return string the class to apply
757 */
758 public function getRowClass(bool $public = false): string
759 {
760 $strclass = ($this->isActive()) ? 'active-account' : 'inactive-account';
761 if ($public === false) {
762 $strclass .= $this->_row_classes;
763 }
764 return $strclass;
765 }
766
767 /**
768 * Get current member due status
769 *
770 * @return string i18n string representing state of due
771 */
772 public function getDues(): string
773 {
774 $ret = '';
775 $never_contributed = false;
776 $now = new \DateTime();
777 // To count the days remaining, the next begin date is required.
778 if ($this->_due_date === null) {
779 $this->_due_date = $now->format('Y-m-d');
780 $never_contributed = true;
781 }
782 $due_date = new \DateTime($this->_due_date);
783 $next_begin_date = clone $due_date;
784 $next_begin_date->add(new \DateInterval('P1D'));
785 $date_diff = $now->diff($next_begin_date);
786 if ($this->isDueFree()) {
787 $ret = _T("Freed of dues");
788 } elseif ($never_contributed === true) {
789 $patterns = array('/%days/', '/%date/');
790 $cdate = new \DateTime($this->_creation_date);
791 $replace = array(
792 $this->_oldness,
793 $cdate->format(__("Y-m-d"))
794 );
795 if ($this->_active) {
796 $ret = preg_replace(
797 $patterns,
798 $replace,
799 _T("Never contributed: Registered %days days ago (since %date)")
800 );
801 } else {
802 $ret = _T("Never contributed");
803 }
804 // Last active or first expired day
805 } elseif ($this->_days_remaining == 0) {
806 if ($date_diff->invert == 0) {
807 $ret = _T("Last day!");
808 } else {
809 $ret = _T("Late since today!");
810 }
811 // Active
812 } elseif ($date_diff->invert == 0 && $this->_days_remaining > 0) {
813 $patterns = array('/%days/', '/%date/');
814 $replace = array(
815 $this->_days_remaining,
816 $due_date->format(__("Y-m-d"))
817 );
818 $ret = preg_replace(
819 $patterns,
820 $replace,
821 _T("%days days remaining (ending on %date)")
822 );
823 // Expired
824 } elseif ($date_diff->invert == 1 && $this->_days_remaining > 0) {
825 $patterns = array('/%days/', '/%date/');
826 $replace = array(
827 // We need the number of days expired, not the number of days remaining.
828 $this->_days_remaining + 1,
829 $due_date->format(__("Y-m-d"))
830 );
831 if ($this->_active) {
832 $ret = preg_replace(
833 $patterns,
834 $replace,
835 _T("Late of %days days (since %date)")
836 );
837 } else {
838 $ret = _T("No longer member");
839 }
840 }
841 return $ret;
842 }
843
844 /**
845 * Retrieve Full name and surname for the specified member id
846 *
847 * @param Db $zdb Database instance
848 * @param integer $id Member id
849 * @param boolean $wid Add member id
850 * @param boolean $wnick Add member nickname
851 *
852 * @return string formatted Name and Surname
853 */
854 public static function getSName(Db $zdb, int $id, bool $wid = false, bool $wnick = false): string
855 {
856 try {
857 $select = $zdb->select(self::TABLE);
858 $select->where([self::PK => $id]);
859
860 $results = $zdb->execute($select);
861 $row = $results->current();
862 return self::getNameWithCase(
863 $row->nom_adh,
864 $row->prenom_adh,
865 false,
866 ($wid === true ? $row->id_adh : false),
867 ($wnick === true ? $row->pseudo_adh : false)
868 );
869 } catch (Throwable $e) {
870 Analog::log(
871 'Cannot get formatted name for member form id `' . $id . '` | ' .
872 $e->getMessage(),
873 Analog::WARNING
874 );
875 throw $e;
876 }
877 }
878
879 /**
880 * Get member name with correct case
881 *
882 * @param string $name Member name
883 * @param string $surname Member surname
884 * @param false|Title $title Member title to show or false
885 * @param false|integer $id Member id to display or false
886 * @param false|string $nick Member nickname to display or false
887 *
888 * @return string
889 */
890 public static function getNameWithCase(
891 ?string $name,
892 ?string $surname,
893 $title = false,
894 $id = false,
895 $nick = false
896 ): string {
897 $str = '';
898
899 if ($title instanceof Title) {
900 $str .= $title->tshort . ' ';
901 }
902
903 $str .= mb_strtoupper($name ?? '', 'UTF-8') . ' ' .
904 ucwords(mb_strtolower($surname ?? '', 'UTF-8'), " \t\r\n\f\v-_|");
905
906 if ($id !== false || !empty($nick)) {
907 $str .= ' (';
908 }
909 if (!empty($nick)) {
910 $str .= $nick;
911 }
912 if ($id !== false) {
913 if (!empty($nick)) {
914 $str .= ', ';
915 }
916 $str .= $id;
917 }
918 if ($id !== false || !empty($nick)) {
919 $str .= ')';
920 }
921 return strip_tags($str);
922 }
923
924 /**
925 * Change password for a given user
926 *
927 * @param Db $zdb Database instance
928 * @param integer $id_adh Member identifier
929 * @param string $pass New password
930 *
931 * @return boolean
932 */
933 public static function updatePassword(Db $zdb, int $id_adh, string $pass): bool
934 {
935 try {
936 $cpass = password_hash($pass, PASSWORD_BCRYPT);
937
938 $update = $zdb->update(self::TABLE);
939 $update->set(
940 array('mdp_adh' => $cpass)
941 )->where([self::PK => $id_adh]);
942 $zdb->execute($update);
943 Analog::log(
944 'Password for `' . $id_adh . '` has been updated.',
945 Analog::DEBUG
946 );
947 return true;
948 } catch (Throwable $e) {
949 Analog::log(
950 'An error occurred while updating password for `' . $id_adh .
951 '` | ' . $e->getMessage(),
952 Analog::ERROR
953 );
954 throw $e;
955 }
956 }
957
958 /**
959 * Get field label
960 *
961 * @param string $field Field name
962 *
963 * @return string
964 */
965 private function getFieldLabel(string $field): string
966 {
967 $label = $this->fields[$field]['label'] ?? '';
968 //replace "&nbsp;"
969 $label = str_replace('&nbsp;', ' ', $label);
970 //remove trailing ':' and then trim
971 $label = trim(trim($label, ':'));
972 return $label;
973 }
974
975 /**
976 * Retrieve fields from database
977 *
978 * @param Db $zdb Database instance
979 *
980 * @return array
981 */
982 public static function getDbFields(Db $zdb): array
983 {
984 $columns = $zdb->getColumns(self::TABLE);
985 $fields = array();
986 foreach ($columns as $col) {
987 $fields[] = $col->getName();
988 }
989 return $fields;
990 }
991
992 /**
993 * Mark as self membership
994 *
995 * @return void
996 */
997 public function setSelfMembership(): void
998 {
999 $this->_self_adh = true;
1000 }
1001
1002 /**
1003 * Is member up to date?
1004 *
1005 * @return boolean
1006 */
1007 public function isUp2Date(): bool
1008 {
1009 if (!$this->isDepEnabled('dues')) {
1010 $this->checkDues();
1011 }
1012
1013 if ($this->isDueFree()) {
1014 //member is due free, he's up to date.
1015 return true;
1016 } else {
1017 //let's check from due date, if present
1018 if ($this->_due_date == null) {
1019 return false;
1020 } else {
1021 $due_date = new \DateTime($this->_due_date);
1022 $now = new \DateTime();
1023 $now->setTime(0, 0, 0);
1024 return $due_date >= $now;
1025 }
1026 }
1027 }
1028
1029 /**
1030 * Set dependencies
1031 *
1032 * @param Preferences $preferences Preferences instance
1033 * @param array $fields Members fields configuration
1034 * @param History $history History instance
1035 *
1036 * @return void
1037 */
1038 public function setDependencies(
1039 Preferences $preferences,
1040 array $fields,
1041 History $history
1042 ) {
1043 $this->preferences = $preferences;
1044 $this->fields = $fields;
1045 $this->history = $history;
1046 }
1047
1048 /**
1049 * Check posted values validity
1050 *
1051 * @param array $values All values to check, basically the $_POST array
1052 * after sending the form
1053 * @param array $required Array of required fields
1054 * @param array $disabled Array of disabled fields
1055 *
1056 * @return true|array
1057 */
1058 public function check(array $values, array $required, array $disabled)
1059 {
1060 global $login;
1061
1062 $this->errors = array();
1063
1064 //Sanitize
1065 foreach ($values as &$rawvalue) {
1066 if (is_string($rawvalue)) {
1067 $rawvalue = strip_tags($rawvalue);
1068 }
1069 }
1070
1071 $fields = self::getDbFields($this->zdb);
1072
1073 //reset company name if needed
1074 if (!isset($values['is_company'])) {
1075 unset($values['is_company']);
1076 $values['societe_adh'] = '';
1077 }
1078
1079 //no parent if checkbox was unchecked
1080 if (
1081 !isset($values['attach'])
1082 && empty($this->_id)
1083 && isset($values['parent_id'])
1084 ) {
1085 unset($values['parent_id']);
1086 }
1087
1088 if (isset($values['duplicate'])) {
1089 //if we're duplicating, keep a trace (if an error occurs)
1090 $this->_duplicate = true;
1091 }
1092
1093 foreach ($fields as $key) {
1094 //first, let's sanitize values
1095 $key = strtolower($key);
1096 $prop = '_' . $this->fields[$key]['propname'];
1097
1098 if (isset($values[$key])) {
1099 $value = $values[$key];
1100 if ($value !== true && $value !== false) {
1101 //@phpstan-ignore-next-line
1102 $value = trim($value ?? '');
1103 }
1104 } elseif (empty($this->_id)) {
1105 switch ($key) {
1106 case 'bool_admin_adh':
1107 case 'bool_exempt_adh':
1108 case 'bool_display_info':
1109 $value = 0;
1110 break;
1111 case 'activite_adh':
1112 //values that are set at object instantiation
1113 $value = true;
1114 break;
1115 case 'date_crea_adh':
1116 case 'sexe_adh':
1117 case 'titre_adh':
1118 case 'id_statut':
1119 case 'pref_lang':
1120 case 'parent_id':
1121 //values that are set at object instantiation
1122 $value = $this->$prop;
1123 break;
1124 case self::PK:
1125 $value = null;
1126 break;
1127 default:
1128 $value = '';
1129 break;
1130 }
1131 } else {
1132 //keep stored value on update
1133 if ($prop != '_password' || isset($values['mdp_adh']) && isset($values['mdp_adh2'])) {
1134 $value = $this->$prop;
1135 } else {
1136 $value = null;
1137 }
1138 }
1139
1140 // if the field is enabled, check it
1141 if (!isset($disabled[$key])) {
1142 // fill up the adherent structure
1143 if ($value !== null && $value !== true && $value !== false && !is_object($value)) {
1144 $value = stripslashes($value);
1145 }
1146 $this->$prop = $value;
1147
1148 // now, check validity
1149 if ($value !== null && $value != '') {
1150 if ($key !== 'mdp_adh') {
1151 $this->validate($key, $value, $values);
1152 }
1153 } elseif (empty($this->_id)) {
1154 //ensure login and password are not empty
1155 if (($key == 'login_adh' || $key == 'mdp_adh') && !isset($required[$key])) {
1156 $p = new Password($this->zdb);
1157 $generated_value = $p->makeRandomPassword(15);
1158 if ($key == 'login_adh') {
1159 //'@' is not permitted in logins
1160 $this->$prop = str_replace('@', 'a', $generated_value);
1161 } else {
1162 $this->$prop = $generated_value;
1163 }
1164 }
1165 }
1166 }
1167 }
1168
1169 //password checks need data to be previously set
1170 if (isset($values['mdp_adh'])) {
1171 $this->validate('mdp_adh', $values['mdp_adh'], $values);
1172 }
1173
1174 // missing required fields?
1175 foreach ($required as $key => $val) {
1176 $prop = '_' . $this->fields[$key]['propname'];
1177
1178 if (!isset($disabled[$key])) {
1179 $mandatory_missing = false;
1180 if (!isset($this->$prop) || $this->$prop == '') {
1181 $mandatory_missing = true;
1182 } elseif ($key === 'titre_adh' && $this->$prop == '-1') {
1183 $mandatory_missing = true;
1184 }
1185
1186 if ($mandatory_missing === true) {
1187 $this->errors[] = str_replace(
1188 '%field',
1189 '<a href="#' . $key . '">' . $this->getFieldLabel($key) . '</a>',
1190 _T("- Mandatory field %field empty.")
1191 );
1192 }
1193 }
1194 }
1195
1196 //attach to/detach from parent
1197 if (isset($values['detach_parent'])) {
1198 $this->_parent = null;
1199 }
1200
1201 if ($login->isGroupManager() && !$login->isAdmin() && !$login->isStaff() && $this->parent_id !== $login->id) {
1202 if (!isset($values['groups_adh'])) {
1203 $this->errors[] = _T('You have to select a group you own!');
1204 } else {
1205 $owned_group = false;
1206 foreach ($values['groups_adh'] as $group) {
1207 list($gid) = explode('|', $group);
1208 if ($login->isGroupManager($gid)) {
1209 $owned_group = true;
1210 }
1211 }
1212 if ($owned_group === false) {
1213 $this->errors[] = _T('You have to select a group you own!');
1214 }
1215 }
1216 }
1217
1218 $this->dynamicsCheck($values, $required, $disabled);
1219 $this->checkSocials($values);
1220
1221 if (count($this->errors) > 0) {
1222 Analog::log(
1223 'Some errors has been thew attempting to edit/store a member' . "\n" .
1224 print_r($this->errors, true),
1225 Analog::ERROR
1226 );
1227 return $this->errors;
1228 } else {
1229 $this->checkDues();
1230
1231 Analog::log(
1232 'Member checked successfully.',
1233 Analog::DEBUG
1234 );
1235 return true;
1236 }
1237 }
1238
1239 /**
1240 * Validate data for given key
1241 * Set valid data in current object, also resets errors list
1242 *
1243 * @param string $field Field name
1244 * @param mixed $value Value we want to set
1245 * @param array $values All values, for some references
1246 *
1247 * @return void
1248 */
1249 public function validate(string $field, $value, array $values): void
1250 {
1251 global $preferences;
1252
1253 $prop = '_' . $this->fields[$field]['propname'];
1254
1255 if ($value === null || (is_string($value) && trim($value) == '')) {
1256 //empty values are OK
1257 $this->$prop = $value;
1258 return;
1259 }
1260
1261 switch ($field) {
1262 // dates
1263 case 'date_crea_adh':
1264 case 'date_modif_adh_':
1265 case 'ddn_adh':
1266 case 'date_echeance':
1267 try {
1268 $d = \DateTime::createFromFormat(__("Y-m-d"), $value);
1269 if ($d === false) {
1270 //try with non localized date
1271 $d = \DateTime::createFromFormat("Y-m-d", $value);
1272 if ($d === false) {
1273 throw new \Exception('Incorrect format');
1274 }
1275 }
1276
1277 if ($field === 'ddn_adh') {
1278 $now = new \DateTime();
1279 $now->setTime(0, 0, 0);
1280 $d->setTime(0, 0, 0);
1281
1282 $diff = $now->diff($d);
1283 $days = (int)$diff->format('%R%a');
1284 if ($days >= 0) {
1285 $this->errors[] = _T('- Birthdate must be set in the past!');
1286 }
1287
1288 $years = (int)$diff->format('%R%Y');
1289 if ($years <= -200) {
1290 $this->errors[] = str_replace(
1291 '%years',
1292 $years * -1,
1293 _T('- Members must be less than 200 years old (currently %years)!')
1294 );
1295 }
1296 }
1297 $this->$prop = $d->format('Y-m-d');
1298 } catch (Throwable $e) {
1299 Analog::log(
1300 'Wrong date format. field: ' . $field .
1301 ', value: ' . $value . ', expected fmt: ' .
1302 __("Y-m-d") . ' | ' . $e->getMessage(),
1303 Analog::INFO
1304 );
1305 $this->errors[] = str_replace(
1306 array(
1307 '%date_format',
1308 '%field'
1309 ),
1310 array(
1311 __("Y-m-d"),
1312 $this->getFieldLabel($field)
1313 ),
1314 _T("- Wrong date format (%date_format) for %field!")
1315 );
1316 }
1317 break;
1318 case 'titre_adh':
1319 if ($value !== null && $value !== '') {
1320 if ($value == '-1') {
1321 $this->$prop = null;
1322 } elseif (!$value instanceof Title) {
1323 $this->$prop = new Title((int)$value);
1324 }
1325 } else {
1326 $this->$prop = null;
1327 }
1328 break;
1329 case 'email_adh':
1330 if (!GaletteMail::isValidEmail($value)) {
1331 $this->errors[] = _T("- Non-valid E-Mail address!") .
1332 ' (' . $this->getFieldLabel($field) . ')';
1333 }
1334 if ($field == 'email_adh') {
1335 try {
1336 $select = $this->zdb->select(self::TABLE);
1337 $select->columns(
1338 array(self::PK)
1339 )->where(array('email_adh' => $value));
1340 if (!empty($this->_id)) {
1341 $select->where->notEqualTo(
1342 self::PK,
1343 $this->_id
1344 );
1345 }
1346
1347 $results = $this->zdb->execute($select);
1348 if ($results->count() !== 0) {
1349 $this->errors[] = _T("- This E-Mail address is already used by another member!");
1350 }
1351 } catch (Throwable $e) {
1352 Analog::log(
1353 'An error occurred checking member email uniqueness.',
1354 Analog::ERROR
1355 );
1356 $this->errors[] = _T("An error has occurred while looking if login already exists.");
1357 }
1358 }
1359 break;
1360 case 'login_adh':
1361 /** FIXME: add a preference for login length */
1362 if (strlen($value) < 2) {
1363 $this->errors[] = str_replace(
1364 '%i',
1365 2,
1366 _T("- The username must be composed of at least %i characters!")
1367 );
1368 } else {
1369 //check if login does not contain the @ character
1370 if (strpos($value, '@') != false) {
1371 $this->errors[] = _T("- The username cannot contain the @ character");
1372 } else {
1373 //check if login is already taken
1374 try {
1375 $select = $this->zdb->select(self::TABLE);
1376 $select->columns(
1377 array(self::PK)
1378 )->where(array('login_adh' => $value));
1379 if (!empty($this->_id)) {
1380 $select->where->notEqualTo(
1381 self::PK,
1382 $this->_id
1383 );
1384 }
1385
1386 $results = $this->zdb->execute($select);
1387 if (
1388 $results->count() !== 0
1389 || $value == $preferences->pref_admin_login
1390 ) {
1391 $this->errors[] = _T("- This username is already in use, please choose another one!");
1392 }
1393 } catch (Throwable $e) {
1394 Analog::log(
1395 'An error occurred checking member login uniqueness.',
1396 Analog::ERROR
1397 );
1398 $this->errors[] = _T("An error has occurred while looking if login already exists.");
1399 }
1400 }
1401 }
1402 break;
1403 case 'mdp_adh':
1404 if (
1405 $this->_self_adh !== true
1406 && (!isset($values['mdp_adh2'])
1407 || $values['mdp_adh2'] != $value)
1408 ) {
1409 $this->errors[] = _T("- The passwords don't match!");
1410 } elseif (
1411 $this->_self_adh === true
1412 && !crypt($value, $values['mdp_crypt']) == $values['mdp_crypt']
1413 ) {
1414 $this->errors[] = _T("Password misrepeated: ");
1415 } else {
1416 $pinfos = password_get_info($value);
1417 //check if value is already a hash
1418 if ($pinfos['algo'] == 0) {
1419 $this->$prop = password_hash(
1420 $value,
1421 PASSWORD_BCRYPT
1422 );
1423
1424 $pwcheck = new \Galette\Util\Password($preferences);
1425 $pwcheck->setAdherent($this);
1426 if (!$pwcheck->isValid($value)) {
1427 $this->errors = array_merge(
1428 $this->errors,
1429 $pwcheck->getErrors()
1430 );
1431 }
1432 }
1433 }
1434 break;
1435 case 'id_statut':
1436 try {
1437 $this->$prop = (int)$value;
1438 //check if status exists
1439 $select = $this->zdb->select(Status::TABLE);
1440 $select->where([Status::PK => $value]);
1441
1442 $results = $this->zdb->execute($select);
1443 $result = $results->current();
1444 if (!$result) {
1445 $this->errors[] = str_replace(
1446 '%id',
1447 $value,
1448 _T("Status #%id does not exists in database.")
1449 );
1450 break;
1451 }
1452 } catch (Throwable $e) {
1453 Analog::log(
1454 'An error occurred checking status existence: ' . $e->getMessage(),
1455 Analog::ERROR
1456 );
1457 $this->errors[] = _T("An error has occurred while looking if status does exists.");
1458 }
1459 break;
1460 case 'sexe_adh':
1461 if (in_array($value, [self::NC, self::MAN, self::WOMAN])) {
1462 $this->$prop = (int)$value;
1463 } else {
1464 $this->errors[] = _T("Gender %gender does not exists!");
1465 }
1466 break;
1467 case 'parent_id':
1468 $this->$prop = ($value instanceof Adherent) ? (int)$value->id : (int)$value;
1469 $this->loadParent();
1470 break;
1471 }
1472 }
1473
1474 /**
1475 * Store the member
1476 *
1477 * @return boolean
1478 */
1479 public function store(): bool
1480 {
1481 global $hist, $emitter, $login;
1482 $event = null;
1483
1484 if (!$login->isAdmin() && !$login->isStaff() && !$login->isGroupManager() && $this->id == '') {
1485 if ($this->preferences->pref_bool_create_member) {
1486 $this->_parent = $login->id;
1487 }
1488 }
1489
1490 try {
1491 $values = array();
1492 $fields = self::getDbFields($this->zdb);
1493
1494 foreach ($fields as $field) {
1495 if (
1496 $field !== 'date_modif_adh'
1497 || empty($this->_id)
1498 ) {
1499 $prop = '_' . $this->fields[$field]['propname'];
1500 if (
1501 ($field === 'bool_admin_adh'
1502 || $field === 'bool_exempt_adh'
1503 || $field === 'bool_display_info'
1504 || $field === 'activite_adh')
1505 && $this->$prop === false
1506 ) {
1507 //Handle booleans for postgres ; bugs #18899 and #19354
1508 $values[$field] = $this->zdb->isPostgres() ? 'false' : 0;
1509 } elseif ($field === 'parent_id') {
1510 //handle parents
1511 if ($this->_parent === null) {
1512 $values['parent_id'] = new Expression('NULL');
1513 } elseif ($this->parent instanceof Adherent) {
1514 $values['parent_id'] = $this->_parent->id;
1515 } else {
1516 $values['parent_id'] = $this->_parent;
1517 }
1518 } else {
1519 $values[$field] = $this->$prop;
1520 }
1521 }
1522 }
1523
1524 //an empty value will cause date to be set to 1901-01-01, a null
1525 //will result in 0000-00-00. We want a database NULL value here.
1526 if (!$this->_birthdate) {
1527 $values['ddn_adh'] = new Expression('NULL');
1528 }
1529 if (!$this->_due_date) {
1530 $values['date_echeance'] = new Expression('NULL');
1531 }
1532
1533 if ($this->_title instanceof Title) {
1534 $values['titre_adh'] = $this->_title->id;
1535 } else {
1536 $values['titre_adh'] = new Expression('NULL');
1537 }
1538
1539 if (!$this->_parent) {
1540 $values['parent_id'] = new Expression('NULL');
1541 }
1542
1543 if (!$this->_number) {
1544 $values['num_adh'] = new Expression('NULL');
1545 }
1546
1547 //fields that cannot be null
1548 $notnull = [
1549 '_surname' => 'prenom_adh',
1550 '_nickname' => 'pseudo_adh',
1551 '_address' => 'adresse_adh',
1552 '_zipcode' => 'cp_adh',
1553 '_town' => 'ville_adh'
1554 ];
1555 foreach ($notnull as $prop => $field) {
1556 if ($this->$prop === null) {
1557 $values[$field] = '';
1558 }
1559 }
1560
1561 $success = false;
1562 if (empty($this->_id)) {
1563 //we're inserting a new member
1564 unset($values[self::PK]);
1565 //set modification date
1566 $this->_modification_date = date('Y-m-d');
1567 $values['date_modif_adh'] = $this->_modification_date;
1568
1569 $insert = $this->zdb->insert(self::TABLE);
1570 $insert->values($values);
1571 $add = $this->zdb->execute($insert);
1572 if ($add->count() > 0) {
1573 $this->_id = $this->zdb->getLastGeneratedValue($this);
1574 $this->_picture = new Picture($this->_id);
1575 // logging
1576 if ($this->_self_adh) {
1577 $hist->add(
1578 _T("Self_subscription as a member: ") .
1579 $this->getNameWithCase($this->_name, $this->_surname),
1580 $this->sname
1581 );
1582 } else {
1583 $hist->add(
1584 _T("Member card added"),
1585 $this->sname
1586 );
1587 }
1588 $success = true;
1589
1590 $event = 'member.add';
1591 } else {
1592 $hist->add(_T("Fail to add new member."));
1593 throw new \Exception(
1594 'An error occurred inserting new member!'
1595 );
1596 }
1597 } else {
1598 //we're editing an existing member
1599 if (!$this->isDueFree()) {
1600 // deadline
1601 $due_date = Contribution::getDueDate($this->zdb, $this->_id);
1602 if ($due_date) {
1603 $values['date_echeance'] = $due_date;
1604 }
1605 }
1606
1607 if (!$this->_password) {
1608 unset($values['mdp_adh']);
1609 }
1610
1611 $update = $this->zdb->update(self::TABLE);
1612 $update->set($values);
1613 $update->where([self::PK => $this->_id]);
1614
1615 $edit = $this->zdb->execute($update);
1616
1617 //edit == 0 does not mean there were an error, but that there
1618 //were nothing to change
1619 if ($edit->count() > 0) {
1620 $this->updateModificationDate();
1621 $hist->add(
1622 _T("Member card updated"),
1623 $this->sname
1624 );
1625 }
1626 $success = true;
1627 $event = 'member.edit';
1628 }
1629
1630 //dynamic fields
1631 if ($success) {
1632 $success = $this->dynamicsStore();
1633 $this->storeSocials($this->id);
1634 }
1635
1636 //send event at the end of process, once all has been stored
1637 if ($event !== null) {
1638 $emitter->dispatch(new GaletteEvent($event, $this));
1639 }
1640 return $success;
1641 } catch (Throwable $e) {
1642 Analog::log(
1643 'Something went wrong :\'( | ' . $e->getMessage() . "\n" .
1644 $e->getTraceAsString(),
1645 Analog::ERROR
1646 );
1647 throw $e;
1648 }
1649 }
1650
1651 /**
1652 * Update member modification date
1653 *
1654 * @return void
1655 */
1656 private function updateModificationDate(): void
1657 {
1658 try {
1659 $modif_date = date('Y-m-d');
1660 $update = $this->zdb->update(self::TABLE);
1661 $update->set(
1662 array('date_modif_adh' => $modif_date)
1663 )->where([self::PK => $this->_id]);
1664
1665 $this->zdb->execute($update);
1666 $this->_modification_date = $modif_date;
1667 } catch (Throwable $e) {
1668 Analog::log(
1669 'Something went wrong updating modif date :\'( | ' .
1670 $e->getMessage() . "\n" . $e->getTraceAsString(),
1671 Analog::ERROR
1672 );
1673 throw $e;
1674 }
1675 }
1676
1677 /**
1678 * Global getter method
1679 *
1680 * @param string $name name of the property we want to retrieve
1681 *
1682 * @return mixed
1683 */
1684 public function __get(string $name)
1685 {
1686 $forbidden = array(
1687 'admin', 'staff', 'due_free', 'appears_in_list', 'active',
1688 'row_classes', 'oldness', 'duplicate', 'groups', 'managed_groups'
1689 );
1690 if (!defined('GALETTE_TESTS')) {
1691 $forbidden[] = 'password'; //keep that for tests only
1692 }
1693
1694 $virtuals = array(
1695 'sadmin', 'sstaff', 'sdue_free', 'sappears_in_list', 'sactive',
1696 'stitle', 'sstatus', 'sfullname', 'sname', 'saddress',
1697 'rbirthdate', 'sgender', 'contribstatus',
1698 );
1699
1700 $socials = array('website', 'msn', 'jabber', 'icq');
1701
1702 if (in_array($name, $forbidden)) {
1703 Analog::log(
1704 'Calling property "' . $name . '" directly is discouraged.',
1705 Analog::WARNING
1706 );
1707 switch ($name) {
1708 case 'admin':
1709 return $this->isAdmin();
1710 case 'staff':
1711 return $this->isStaff();
1712 case 'due_free':
1713 return $this->isDueFree();
1714 case 'appears_in_list':
1715 return $this->appearsInMembersList();
1716 case 'active':
1717 return $this->isActive();
1718 case 'duplicate':
1719 return $this->isDuplicate();
1720 case 'groups':
1721 return $this->getGroups();
1722 case 'managed_groups':
1723 return $this->getManagedGroups();
1724 default:
1725 throw new \RuntimeException("Call to __get for '$name' is forbidden!");
1726 }
1727 }
1728
1729 if (in_array($name, $virtuals)) {
1730 if (substr($name, 0, 1) !== '_') {
1731 $real = '_' . substr($name, 1);
1732 } else {
1733 $real = $name;
1734 }
1735 switch ($name) {
1736 case 'sadmin':
1737 return (($this->isAdmin()) ? _T("Yes") : _T("No"));
1738 case 'sdue_free':
1739 return (($this->isDueFree()) ? _T("Yes") : _T("No"));
1740 case 'sappears_in_list':
1741 return (($this->appearsInMembersList()) ? _T("Yes") : _T("No"));
1742 case 'sstaff':
1743 return (($this->$real) ? _T("Yes") : _T("No"));
1744 case 'sactive':
1745 return (($this->isActive()) ? _T("Active") : _T("Inactive"));
1746 case 'stitle':
1747 if (isset($this->_title) && $this->_title instanceof Title) {
1748 return $this->_title->tshort;
1749 } else {
1750 return null;
1751 }
1752 case 'sstatus':
1753 $status = new Status($this->zdb);
1754 return $status->getLabel($this->_status);
1755 case 'sfullname':
1756 return $this->getNameWithCase(
1757 $this->_name,
1758 $this->_surname,
1759 (isset($this->_title) ? $this->title : false)
1760 );
1761 case 'saddress':
1762 $address = $this->_address;
1763 return $address;
1764 case 'sname':
1765 return $this->getNameWithCase($this->_name, $this->_surname);
1766 case 'rbirthdate':
1767 return $this->_birthdate;
1768 case 'sgender':
1769 switch ($this->gender) {
1770 case self::MAN:
1771 return _T('Man');
1772 case self::WOMAN:
1773 return _T('Woman');
1774 default:
1775 return _T('Unspecified');
1776 }
1777 case 'contribstatus':
1778 return $this->getDues();
1779 }
1780 }
1781
1782 //for backward compatibility
1783 if (in_array($name, $socials)) {
1784 $values = Social::getListForMember($this->_id, $name);
1785 return $values[0] ?? null;
1786 }
1787
1788 if (substr($name, 0, 1) !== '_') {
1789 $rname = '_' . $name;
1790 } else {
1791 $rname = $name;
1792 }
1793
1794 switch ($name) {
1795 case 'id':
1796 case 'id_statut':
1797 if ($this->$rname !== null) {
1798 return (int)$this->$rname;
1799 } else {
1800 return null;
1801 }
1802 case 'address':
1803 return $this->$rname ?? '';
1804 case 'birthdate':
1805 case 'creation_date':
1806 case 'modification_date':
1807 case 'due_date':
1808 if ($this->$rname != '') {
1809 try {
1810 $d = new \DateTime($this->$rname);
1811 return $d->format(__("Y-m-d"));
1812 } catch (Throwable $e) {
1813 //oops, we've got a bad date :/
1814 Analog::log(
1815 'Bad date (' . $this->$rname . ') | ' .
1816 $e->getMessage(),
1817 Analog::INFO
1818 );
1819 return $this->$rname;
1820 }
1821 }
1822 break;
1823 case 'parent_id':
1824 return ($this->_parent instanceof Adherent) ? (int)$this->_parent->id : (int)$this->_parent;
1825 default:
1826 if (!property_exists($this, $rname)) {
1827 Analog::log(
1828 "Unknown property '$rname'",
1829 Analog::WARNING
1830 );
1831 return null;
1832 } else {
1833 return $this->$rname;
1834 }
1835 }
1836 }
1837
1838 /**
1839 * Global isset method
1840 * Required for twig to access properties via __get
1841 *
1842 * @param string $name name of the property we want to retrieve
1843 *
1844 * @return bool
1845 */
1846 public function __isset(string $name)
1847 {
1848 $forbidden = array(
1849 'admin', 'staff', 'due_free', 'appears_in_list', 'active',
1850 'row_classes', 'oldness', 'duplicate', 'groups', 'managed_groups'
1851 );
1852 if (!defined('GALETTE_TESTS')) {
1853 $forbidden[] = 'password'; //keep that for tests only
1854 }
1855
1856 $virtuals = array(
1857 'sadmin', 'sstaff', 'sdue_free', 'sappears_in_list', 'sactive',
1858 'stitle', 'sstatus', 'sfullname', 'sname', 'saddress',
1859 'rbirthdate', 'sgender', 'contribstatus',
1860 );
1861
1862 $socials = array('website', 'msn', 'jabber', 'icq');
1863
1864 if (in_array($name, $forbidden)) {
1865 Analog::log(
1866 'Calling property "' . $name . '" directly is discouraged.',
1867 Analog::WARNING
1868 );
1869 switch ($name) {
1870 case 'admin':
1871 case 'staff':
1872 case 'due_free':
1873 case 'appears_in_list':
1874 case 'active':
1875 case 'duplicate':
1876 case 'groups':
1877 case 'managed_groups':
1878 return true;
1879 }
1880
1881 return false;
1882 }
1883
1884 if (in_array($name, $virtuals)) {
1885 return true;
1886 }
1887
1888 //for backward compatibility
1889 if (in_array($name, $socials)) {
1890 return true;
1891 }
1892
1893 if (substr($name, 0, 1) !== '_') {
1894 $rname = '_' . $name;
1895 } else {
1896 $rname = $name;
1897 }
1898
1899 switch ($name) {
1900 case 'id':
1901 case 'id_statut':
1902 case 'address':
1903 case 'birthdate':
1904 case 'creation_date':
1905 case 'modification_date':
1906 case 'due_date':
1907 case 'parent_id':
1908 return true;
1909 default:
1910 return property_exists($this, $rname);
1911 }
1912 }
1913
1914 /**
1915 * Get member email
1916 * If member does not have an email address, but is attached to
1917 * another member, we'll take information from its parent.
1918 *
1919 * @return string
1920 */
1921 public function getEmail(): string
1922 {
1923 $email = $this->_email;
1924 if (empty($email) && $this->hasParent()) {
1925 $this->loadParent();
1926 $email = $this->parent->email;
1927 }
1928
1929 return $email ?? '';
1930 }
1931
1932 /**
1933 * Get member address.
1934 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1935 *
1936 * @return string
1937 */
1938 public function getAddress(): string
1939 {
1940 $address = $this->_address;
1941 if (empty($address) && $this->hasParent()) {
1942 $this->loadParent();
1943 $address = $this->parent->address;
1944 }
1945
1946 return $address ?? '';
1947 }
1948
1949 /**
1950 * Get member zipcode.
1951 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1952 *
1953 * @return string
1954 */
1955 public function getZipcode(): string
1956 {
1957 $address = $this->_address;
1958 $zip = $this->_zipcode;
1959 if (empty($address) && $this->hasParent()) {
1960 $this->loadParent();
1961 $zip = $this->parent->zipcode;
1962 }
1963
1964 return $zip ?? '';
1965 }
1966
1967 /**
1968 * Get member town.
1969 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1970 *
1971 * @return string
1972 */
1973 public function getTown(): string
1974 {
1975 $address = $this->_address;
1976 $town = $this->_town;
1977 if (empty($address) && $this->hasParent()) {
1978 $this->loadParent();
1979 $town = $this->parent->town;
1980 }
1981
1982 return $town ?? '';
1983 }
1984
1985 /**
1986 * Get member country.
1987 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1988 *
1989 * @return string
1990 */
1991 public function getCountry(): string
1992 {
1993 $address = $this->_address;
1994 $country = $this->_country;
1995 if (empty($address) && $this->hasParent()) {
1996 $this->loadParent();
1997 $country = $this->parent->country;
1998 }
1999
2000 return $country ?? '';
2001 }
2002
2003 /**
2004 * Get member age
2005 *
2006 * @return string
2007 */
2008 public function getAge(): string
2009 {
2010 if ($this->_birthdate == null) {
2011 return '';
2012 }
2013
2014 $d = \DateTime::createFromFormat('Y-m-d', $this->_birthdate);
2015 if ($d === false) {
2016 Analog::log(
2017 'Invalid birthdate: ' . $this->_birthdate,
2018 Analog::ERROR
2019 );
2020 return '';
2021 }
2022
2023 return str_replace(
2024 '%age',
2025 $d->diff(new \DateTime())->y,
2026 _T(' (%age years old)')
2027 );
2028 }
2029
2030 /**
2031 * Get parent inherited fields
2032 *
2033 * @return array
2034 */
2035 public function getParentFields(): array
2036 {
2037 return $this->parent_fields;
2038 }
2039
2040 /**
2041 * Handle files (photo and dynamics files)
2042 *
2043 * @param array $files Files sent
2044 * @param array $cropping Cropping properties
2045 *
2046 * @return array|true
2047 */
2048 public function handleFiles(array $files, array $cropping = null)
2049 {
2050 $this->errors = [];
2051 // picture upload
2052 if (isset($files['photo'])) {
2053 if ($files['photo']['error'] === UPLOAD_ERR_OK) {
2054 if ($files['photo']['tmp_name'] != '') {
2055 if (is_uploaded_file($files['photo']['tmp_name'])) {
2056 if ($this->preferences->pref_force_picture_ratio == 1 && isset($cropping)) {
2057 $res = $this->picture->store($files['photo'], false, $cropping);
2058 } else {
2059 $res = $this->picture->store($files['photo']);
2060 }
2061 if ($res < 0) {
2062 $this->errors[]
2063 = $this->picture->getErrorMessage($res);
2064 }
2065 }
2066 }
2067 } elseif ($files['photo']['error'] !== UPLOAD_ERR_NO_FILE) {
2068 Analog::log(
2069 $this->picture->getPhpErrorMessage($files['photo']['error']),
2070 Analog::WARNING
2071 );
2072 $this->errors[] = $this->picture->getPhpErrorMessage(
2073 $files['photo']['error']
2074 );
2075 }
2076 }
2077 $this->dynamicsFiles($files);
2078
2079 if (count($this->errors) > 0) {
2080 Analog::log(
2081 'Some errors has been thew attempting to edit/store a member files' . "\n" .
2082 print_r($this->errors, true),
2083 Analog::ERROR
2084 );
2085 return $this->errors;
2086 } else {
2087 return true;
2088 }
2089 }
2090
2091 /**
2092 * Set member as duplicate
2093 *
2094 * @return void
2095 */
2096 public function setDuplicate(): void
2097 {
2098 //mark as duplicated
2099 $this->_duplicate = true;
2100 $infos = $this->_others_infos_admin;
2101 $this->_others_infos_admin = str_replace(
2102 ['%name', '%id'],
2103 [$this->sname, $this->_id],
2104 _T('Duplicated from %name (%id)')
2105 );
2106 if (!empty($infos)) {
2107 $this->_others_infos_admin .= "\n" . $infos;
2108 }
2109 //drop id_adh
2110 $this->_id = null;
2111 //drop email, must be unique
2112 $this->_email = null;
2113 //drop creation date
2114 $this->_creation_date = date("Y-m-d");
2115 //drop login
2116 $this->_login = null;
2117 //reset picture
2118 $this->_picture = new Picture();
2119 //remove birthdate
2120 $this->_birthdate = null;
2121 //remove surname
2122 $this->_surname = null;
2123 //not admin
2124 $this->_admin = false;
2125 //not due free
2126 $this->_due_free = false;
2127 }
2128
2129 /**
2130 * Get current errors
2131 *
2132 * @return array
2133 */
2134 public function getErrors(): array
2135 {
2136 return $this->errors;
2137 }
2138
2139 /**
2140 * Get user groups
2141 *
2142 * @return array
2143 */
2144 public function getGroups(): array
2145 {
2146 if (!$this->isDepEnabled('groups')) {
2147 $this->loadGroups();
2148 }
2149 return $this->_groups;
2150 }
2151
2152 /**
2153 * Get user managed groups
2154 *
2155 * @return array
2156 */
2157 public function getManagedGroups(): array
2158 {
2159 if (!$this->isDepEnabled('groups')) {
2160 $this->loadGroups();
2161 }
2162 return $this->_managed_groups;
2163 }
2164
2165 /**
2166 * Can current logged-in user create member
2167 *
2168 * @param Login $login Login instance
2169 *
2170 * @return boolean
2171 */
2172 public function canCreate(Login $login): bool
2173 {
2174 global $preferences;
2175
2176 if ($this->id && $login->id == $this->id || $login->isAdmin() || $login->isStaff()) {
2177 return true;
2178 }
2179
2180 if ($preferences->pref_bool_groupsmanagers_create_member && $login->isGroupManager()) {
2181 return true;
2182 }
2183
2184 if ($preferences->pref_bool_create_member && $login->isLogged()) {
2185 return true;
2186 }
2187
2188 return false;
2189 }
2190
2191 /**
2192 * Can current logged-in user edit member
2193 *
2194 * @param Login $login Login instance
2195 *
2196 * @return boolean
2197 */
2198 public function canEdit(Login $login): bool
2199 {
2200 global $preferences;
2201
2202 //admin and staff users can edit, as well as member itself
2203 if ($this->id && $login->id == $this->id || $login->isAdmin() || $login->isStaff()) {
2204 return true;
2205 }
2206
2207 //parent can edit their child cards
2208 if ($this->hasParent() && $this->parent_id === $login->id) {
2209 return true;
2210 }
2211
2212 //group managers can edit members of groups they manage when pref is on
2213 if ($preferences->pref_bool_groupsmanagers_edit_member && $login->isGroupManager()) {
2214 foreach ($this->getGroups() as $g) {
2215 if ($login->isGroupManager($g->getId())) {
2216 return true;
2217 }
2218 }
2219 }
2220
2221 return false;
2222 }
2223
2224 /**
2225 * Can current logged-in user display member
2226 *
2227 * @param Login $login Login instance
2228 *
2229 * @return boolean
2230 */
2231 public function canShow(Login $login): bool
2232 {
2233 //group managers can show members of groups they manage
2234 if ($login->isGroupManager()) {
2235 foreach ($this->getGroups() as $g) {
2236 if ($login->isGroupManager($g->getId())) {
2237 return true;
2238 }
2239 }
2240 }
2241
2242 return $this->canEdit($login);
2243 }
2244
2245 /**
2246 * Are we currently duplicated a member?
2247 *
2248 * @return boolean
2249 */
2250 public function isDuplicate(): bool
2251 {
2252 return $this->_duplicate;
2253 }
2254
2255 /**
2256 * Flag creation mail sending
2257 *
2258 * @param boolean $send True (default) to send creation email
2259 *
2260 * @return Adherent
2261 */
2262 public function setSendmail(bool $send = true): self
2263 {
2264 $this->sendmail = $send;
2265 return $this;
2266 }
2267
2268 /**
2269 * Should we send administrative emails to member?
2270 *
2271 * @return boolean
2272 */
2273 public function sendEMail(): bool
2274 {
2275 return $this->sendmail;
2276 }
2277
2278 /**
2279 * Set member parent
2280 *
2281 * @param integer $id Parent identifier
2282 *
2283 * @return $this
2284 */
2285 public function setParent(int $id): self
2286 {
2287 $this->_parent = $id;
2288 $this->loadParent();
2289 return $this;
2290 }
2291 }