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