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