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