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