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