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