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