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