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