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