]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/Adherent.php
Prevent some possible XSS
[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-2014 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-2014 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-2014 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($r->parent_id);
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 if ($this->isDueFree()) {
769 $ret = _T("Freed of dues");
770 } elseif ($this->_due_date == '') {
771 $patterns = array('/%days/', '/%date/');
772 $cdate = new \DateTime($this->_creation_date);
773 $replace = array(
774 $this->_oldness,
775 $cdate->format(__("Y-m-d"))
776 );
777 if ($this->_active) {
778 $ret = preg_replace(
779 $patterns,
780 $replace,
781 _T("Never contributed: Registered %days days ago (since %date)")
782 );
783 } else {
784 $ret = _T("Never contributed");
785 }
786 } elseif ($this->_days_remaining == 0) {
787 $ret = _T("Last day!");
788 } elseif ($this->_days_remaining < 0) {
789 $patterns = array('/%days/', '/%date/');
790 $ddate = new \DateTime($this->_due_date);
791 $replace = array(
792 $this->_days_remaining * -1,
793 $ddate->format(__("Y-m-d"))
794 );
795 if ($this->_active) {
796 $ret = preg_replace(
797 $patterns,
798 $replace,
799 _T("Late of %days days (since %date)")
800 );
801 } else {
802 $ret = _T("No longer member");
803 }
804 } else {
805 $patterns = array('/%days/', '/%date/');
806 $ddate = new \DateTime($this->_due_date);
807 $replace = array(
808 $this->_days_remaining,
809 $ddate->format(__("Y-m-d"))
810 );
811 $ret = preg_replace(
812 $patterns,
813 $replace,
814 _T("%days days remaining (ending on %date)")
815 );
816 }
817 return $ret;
818 }
819
820 /**
821 * Retrieve Full name and surname for the specified member id
822 *
823 * @param Db $zdb Database instance
824 * @param integer $id Member id
825 * @param boolean $wid Add member id
826 * @param boolean $wnick Add member nickname
827 *
828 * @return string formatted Name and Surname
829 */
830 public static function getSName($zdb, $id, $wid = false, $wnick = false)
831 {
832 try {
833 $select = $zdb->select(self::TABLE);
834 $select->where(self::PK . ' = ' . $id);
835
836 $results = $zdb->execute($select);
837 $row = $results->current();
838 return self::getNameWithCase(
839 $row->nom_adh,
840 $row->prenom_adh,
841 false,
842 ($wid === true ? $row->id_adh : false),
843 ($wnick === true ? $row->pseudo_adh : false)
844 );
845 } catch (Throwable $e) {
846 Analog::log(
847 'Cannot get formatted name for member form id `' . $id . '` | ' .
848 $e->getMessage(),
849 Analog::WARNING
850 );
851 return false;
852 }
853 }
854
855 /**
856 * Get member name with correct case
857 *
858 * @param string $name Member name
859 * @param string $surname Mmeber surname
860 * @param false|Title $title Member title to show or false
861 * @param false|integer $id Member id to display or false
862 * @param false|string $nick Member nickname to display or false
863 *
864 * @return string
865 */
866 public static function getNameWithCase($name, $surname, $title = false, $id = false, $nick = false)
867 {
868 $str = '';
869
870 if ($title !== false && $title instanceof Title) {
871 $str .= $title->tshort . ' ';
872 }
873
874 $str .= mb_strtoupper($name, 'UTF-8') . ' ' .
875 ucwords(mb_strtolower($surname, 'UTF-8'), " \t\r\n\f\v-_|");
876
877 if ($id !== false || $nick !== false) {
878 $str .= ' (';
879 }
880 if ($nick !== false) {
881 $str .= $nick;
882 }
883 if ($id !== false) {
884 if ($nick !== false && !empty($nick)) {
885 $str .= ', ';
886 }
887 $str .= $id;
888 }
889 if ($id !== false || $nick !== false) {
890 $str .= ')';
891 }
892 return strip_tags($str);
893 }
894
895 /**
896 * Change password for a given user
897 *
898 * @param Db $zdb Database instance
899 * @param string $id_adh Member identifier
900 * @param string $pass New password
901 *
902 * @return boolean
903 */
904 public static function updatePassword(Db $zdb, $id_adh, $pass)
905 {
906 try {
907 $cpass = password_hash($pass, PASSWORD_BCRYPT);
908
909 $update = $zdb->update(self::TABLE);
910 $update->set(
911 array('mdp_adh' => $cpass)
912 )->where(self::PK . ' = ' . $id_adh);
913 $zdb->execute($update);
914 Analog::log(
915 'Password for `' . $id_adh . '` has been updated.',
916 Analog::DEBUG
917 );
918 return true;
919 } catch (Throwable $e) {
920 Analog::log(
921 'An error occurred while updating password for `' . $id_adh .
922 '` | ' . $e->getMessage(),
923 Analog::ERROR
924 );
925 return false;
926 }
927 }
928
929 /**
930 * Get field label
931 *
932 * @param string $field Field name
933 *
934 * @return string
935 */
936 private function getFieldLabel($field)
937 {
938 $label = $this->fields[$field]['label'];
939 //remove trailing ':' and then nbsp (for french at least)
940 $label = trim(trim($label, ':'), '&nbsp;');
941 return $label;
942 }
943
944 /**
945 * Retrieve fields from database
946 *
947 * @param Db $zdb Database instance
948 *
949 * @return array
950 */
951 public static function getDbFields(Db $zdb)
952 {
953 $columns = $zdb->getColumns(self::TABLE);
954 $fields = array();
955 foreach ($columns as $col) {
956 $fields[] = $col->getName();
957 }
958 return $fields;
959 }
960
961 /**
962 * Mark as self membership
963 *
964 * @return void
965 */
966 public function setSelfMembership()
967 {
968 $this->_self_adh = true;
969 }
970
971 /**
972 * Is member up to date?
973 *
974 * @return boolean
975 */
976 public function isUp2Date()
977 {
978 if ($this->_deps['dues']) {
979 if ($this->isDueFree()) {
980 //member is due free, he's up to date.
981 return true;
982 } else {
983 //let's check from end date, if present
984 if ($this->_due_date == null) {
985 return false;
986 } else {
987 $ech = new \DateTime($this->_due_date);
988 $now = new \DateTime();
989 $now->setTime(0, 0, 0);
990 return $ech >= $now;
991 }
992 }
993 } else {
994 throw new \RuntimeException(
995 'Cannot check if member is up to date, dues deps is disabled!'
996 );
997 }
998 }
999
1000 /**
1001 * Set dependencies
1002 *
1003 * @param Preferences $preferences Preferences instance
1004 * @param array $fields Members fields configuration
1005 * @param History $history History instance
1006 *
1007 * @return void
1008 */
1009 public function setDependencies(
1010 Preferences $preferences,
1011 array $fields,
1012 History $history
1013 ) {
1014 $this->preferences = $preferences;
1015 $this->fields = $fields;
1016 $this->history = $history;
1017 }
1018
1019 /**
1020 * Check posted values validity
1021 *
1022 * @param array $values All values to check, basically the $_POST array
1023 * after sending the form
1024 * @param array $required Array of required fields
1025 * @param array $disabled Array of disabled fields
1026 *
1027 * @return true|array
1028 */
1029 public function check($values, $required, $disabled)
1030 {
1031 $this->errors = array();
1032
1033 $fields = self::getDbFields($this->zdb);
1034
1035 //reset company name if needeed
1036 if (!isset($values['is_company'])) {
1037 unset($values['is_company']);
1038 $values['societe_adh'] = '';
1039 }
1040
1041 //no parent if checkbox was unchecked
1042 if (
1043 !isset($values['attach'])
1044 && empty($this->_id)
1045 && isset($values['parent_id'])
1046 ) {
1047 unset($values['parent_id']);
1048 }
1049
1050 if (isset($values['duplicate'])) {
1051 //if we're duplicating, keep a trace (if an error occurs)
1052 $this->_duplicate = true;
1053 }
1054
1055 foreach ($fields as $key) {
1056 //first of all, let's sanitize values
1057 $key = strtolower($key);
1058 $prop = '_' . $this->fields[$key]['propname'];
1059
1060 if (isset($values[$key])) {
1061 $value = $values[$key];
1062 if ($value !== true && $value !== false) {
1063 $value = trim($value);
1064 }
1065 } elseif ($this->_id == '' || $this->_id == null) {
1066 switch ($key) {
1067 case 'bool_admin_adh':
1068 case 'bool_exempt_adh':
1069 case 'bool_display_info':
1070 $value = 0;
1071 break;
1072 case 'activite_adh':
1073 //values that are setted at object instanciation
1074 $value = true;
1075 break;
1076 case 'date_crea_adh':
1077 case 'sexe_adh':
1078 case 'titre_adh':
1079 case 'id_statut':
1080 case 'pref_lang':
1081 case 'parent_id':
1082 //values that are setted at object instanciation
1083 $value = $this->$prop;
1084 break;
1085 default:
1086 $value = '';
1087 }
1088 } else {
1089 //keep stored value on update
1090 if ($prop != '_password' || isset($values['mdp_adh']) && isset($values['mdp_adh2'])) {
1091 $value = $this->$prop;
1092 } else {
1093 $value = null;
1094 }
1095 }
1096
1097 // if the field is enabled, check it
1098 if (!isset($disabled[$key])) {
1099 // fill up the adherent structure
1100 if ($value !== null && $value !== true && $value !== false && !is_object($value)) {
1101 $value = stripslashes($value);
1102 }
1103 $this->$prop = $value;
1104
1105 // now, check validity
1106 if ($value !== null && $value != '') {
1107 $this->validate($key, $value, $values);
1108 } elseif (
1109 ($key == 'login_adh' && !isset($required['login_adh']))
1110 || ($key == 'mdp_adh' && !isset($required['mdp_adh']))
1111 && !isset($this->_id)
1112 ) {
1113 $p = new Password($this->zdb);
1114 $this->$prop = $p->makeRandomPassword(15);
1115 }
1116 }
1117 }
1118
1119 // missing required fields?
1120 foreach ($required as $key => $val) {
1121 $prop = '_' . $this->fields[$key]['propname'];
1122
1123 if (!isset($disabled[$key])) {
1124 $mandatory_missing = false;
1125 if (!isset($this->$prop) || $this->$prop == '') {
1126 $mandatory_missing = true;
1127 } elseif ($key === 'titre_adh' && $this->$prop == '-1') {
1128 $mandatory_missing = true;
1129 }
1130
1131 if ($mandatory_missing === true) {
1132 $this->errors[] = str_replace(
1133 '%field',
1134 '<a href="#' . $key . '">' . $this->getFieldLabel($key) . '</a>',
1135 _T("- Mandatory field %field empty.")
1136 );
1137 }
1138 }
1139 }
1140
1141 //attach to/detach from parent
1142 if (isset($values['detach_parent'])) {
1143 $this->_parent = null;
1144 }
1145
1146 $this->dynamicsCheck($values, $required, $disabled);
1147
1148 if (count($this->errors) > 0) {
1149 Analog::log(
1150 'Some errors has been throwed attempting to edit/store a member' . "\n" .
1151 print_r($this->errors, true),
1152 Analog::ERROR
1153 );
1154 return $this->errors;
1155 } else {
1156 $this->checkDues();
1157
1158 Analog::log(
1159 'Member checked successfully.',
1160 Analog::DEBUG
1161 );
1162 return true;
1163 }
1164 }
1165
1166 /**
1167 * Validate data for given key
1168 * Set valid data in current object, also resets errors list
1169 *
1170 * @param string $field Field name
1171 * @param mixed $value Value we want to set
1172 * @param array $values All values, for some references
1173 *
1174 * @return void
1175 */
1176 public function validate($field, $value, $values)
1177 {
1178 global $preferences;
1179
1180 $prop = '_' . $this->fields[$field]['propname'];
1181
1182 if ($value === null || (is_string($value) && trim($value) == '')) {
1183 //empty values are OK
1184 $this->$prop = $value;
1185 return;
1186 }
1187
1188 switch ($field) {
1189 // dates
1190 case 'date_crea_adh':
1191 case 'date_modif_adh_':
1192 case 'ddn_adh':
1193 case 'date_echeance':
1194 try {
1195 $d = \DateTime::createFromFormat(__("Y-m-d"), $value);
1196 if ($d === false) {
1197 //try with non localized date
1198 $d = \DateTime::createFromFormat("Y-m-d", $value);
1199 if ($d === false) {
1200 throw new \Exception('Incorrect format');
1201 }
1202 }
1203
1204 if ($field === 'ddn_adh') {
1205 $now = new \DateTime();
1206 $now->setTime(0, 0, 0);
1207 $d->setTime(0, 0, 0);
1208
1209 $diff = $now->diff($d);
1210 $days = (int)$diff->format('%R%a');
1211 if ($days >= 0) {
1212 $this->errors[] = _T('- Birthdate must be set in the past!');
1213 }
1214
1215 $years = (int)$diff->format('%R%Y');
1216 if ($years <= -200) {
1217 $this->errors[] = str_replace(
1218 '%years',
1219 $years * -1,
1220 _T('- Members must be less than 200 years old (currently %years)!')
1221 );
1222 }
1223 }
1224 $this->$prop = $d->format('Y-m-d');
1225 } catch (Throwable $e) {
1226 Analog::log(
1227 'Wrong date format. field: ' . $field .
1228 ', value: ' . $value . ', expected fmt: ' .
1229 __("Y-m-d") . ' | ' . $e->getMessage(),
1230 Analog::INFO
1231 );
1232 $this->errors[] = str_replace(
1233 array(
1234 '%date_format',
1235 '%field'
1236 ),
1237 array(
1238 __("Y-m-d"),
1239 $this->getFieldLabel($field)
1240 ),
1241 _T("- Wrong date format (%date_format) for %field!")
1242 );
1243 }
1244 break;
1245 case 'titre_adh':
1246 if ($value !== null && $value !== '') {
1247 if ($value == '-1') {
1248 $this->$prop = null;
1249 } elseif (!$value instanceof Title) {
1250 $this->$prop = new Title((int)$value);
1251 }
1252 } else {
1253 $this->$prop = null;
1254 }
1255 break;
1256 case 'email_adh':
1257 case 'msn_adh':
1258 if (!GaletteMail::isValidEmail($value)) {
1259 $this->errors[] = _T("- Non-valid E-Mail address!") .
1260 ' (' . $this->getFieldLabel($field) . ')';
1261 }
1262 if ($field == 'email_adh') {
1263 try {
1264 $select = $this->zdb->select(self::TABLE);
1265 $select->columns(
1266 array(self::PK)
1267 )->where(array('email_adh' => $value));
1268 if ($this->_id != '' && $this->_id != null) {
1269 $select->where(
1270 self::PK . ' != ' . $this->_id
1271 );
1272 }
1273
1274 $results = $this->zdb->execute($select);
1275 if ($results->count() !== 0) {
1276 $this->errors[] = _T("- This E-Mail address is already used by another member!");
1277 }
1278 } catch (Throwable $e) {
1279 Analog::log(
1280 'An error occurred checking member email unicity.',
1281 Analog::ERROR
1282 );
1283 $this->errors[] = _T("An error has occurred while looking if login already exists.");
1284 }
1285 }
1286 break;
1287 case 'url_adh':
1288 if ($value == 'http://') {
1289 $this->$prop = '';
1290 } elseif (!isValidWebUrl($value)) {
1291 $this->errors[] = _T("- Non-valid Website address! Maybe you've skipped the http://?");
1292 }
1293 break;
1294 case 'login_adh':
1295 /** FIXME: add a preference for login lenght */
1296 if (strlen($value) < 2) {
1297 $this->errors[] = str_replace(
1298 '%i',
1299 2,
1300 _T("- The username must be composed of at least %i characters!")
1301 );
1302 } else {
1303 //check if login does not contain the @ character
1304 if (strpos($value, '@') != false) {
1305 $this->errors[] = _T("- The username cannot contain the @ character");
1306 } else {
1307 //check if login is already taken
1308 try {
1309 $select = $this->zdb->select(self::TABLE);
1310 $select->columns(
1311 array(self::PK)
1312 )->where(array('login_adh' => $value));
1313 if ($this->_id != '' && $this->_id != null) {
1314 $select->where(
1315 self::PK . ' != ' . $this->_id
1316 );
1317 }
1318
1319 $results = $this->zdb->execute($select);
1320 if (
1321 $results->count() !== 0
1322 || $value == $preferences->pref_admin_login
1323 ) {
1324 $this->errors[] = _T("- This username is already in use, please choose another one!");
1325 }
1326 } catch (Throwable $e) {
1327 Analog::log(
1328 'An error occurred checking member login unicity.',
1329 Analog::ERROR
1330 );
1331 $this->errors[] = _T("An error has occurred while looking if login already exists.");
1332 }
1333 }
1334 }
1335 break;
1336 case 'mdp_adh':
1337 if (
1338 $this->_self_adh !== true
1339 && (!isset($values['mdp_adh2'])
1340 || $values['mdp_adh2'] != $value)
1341 ) {
1342 $this->errors[] = _T("- The passwords don't match!");
1343 } elseif (
1344 $this->_self_adh === true
1345 && !crypt($value, $values['mdp_crypt']) == $values['mdp_crypt']
1346 ) {
1347 $this->errors[] = _T("Password misrepeated: ");
1348 } else {
1349 $pinfos = password_get_info($value);
1350 //check if value is already a hash
1351 if ($pinfos['algo'] == 0) {
1352 $this->$prop = password_hash(
1353 $value,
1354 PASSWORD_BCRYPT
1355 );
1356
1357 $pwcheck = new \Galette\Util\Password($preferences);
1358 $pwcheck->setAdherent($this);
1359 if (!$pwcheck->isValid($value)) {
1360 $this->errors = array_merge(
1361 $this->errors,
1362 $pwcheck->getErrors()
1363 );
1364 }
1365 }
1366 }
1367 break;
1368 case 'id_statut':
1369 try {
1370 $this->$prop = (int)$value;
1371 //check if status exists
1372 $select = $this->zdb->select(Status::TABLE);
1373 $select->where(Status::PK . '= ' . $value);
1374
1375 $results = $this->zdb->execute($select);
1376 $result = $results->current();
1377 if (!$result) {
1378 $this->errors[] = str_replace(
1379 '%id',
1380 $value,
1381 _T("Status #%id does not exists in database.")
1382 );
1383 break;
1384 }
1385 } catch (Throwable $e) {
1386 Analog::log(
1387 'An error occurred checking status existance: ' . $e->getMessage(),
1388 Analog::ERROR
1389 );
1390 $this->errors[] = _T("An error has occurred while looking if status does exists.");
1391 }
1392 break;
1393 case 'sexe_adh':
1394 if (in_array($value, [self::NC, self::MAN, self::WOMAN])) {
1395 $this->$prop = (int)$value;
1396 } else {
1397 $this->errors[] = _T("Gender %gender does not exists!");
1398 }
1399 break;
1400 case 'parent_id':
1401 $this->$prop = ($value instanceof Adherent) ? (int)$value->id : (int)$value;
1402 $this->loadParent();
1403 break;
1404 }
1405 }
1406
1407 /**
1408 * Store the member
1409 *
1410 * @return boolean
1411 */
1412 public function store()
1413 {
1414 global $hist, $emitter;
1415 $event = null;
1416
1417 try {
1418 $values = array();
1419 $fields = self::getDbFields($this->zdb);
1420
1421 foreach ($fields as $field) {
1422 if (
1423 $field !== 'date_modif_adh'
1424 || !isset($this->_id)
1425 || $this->_id == ''
1426 ) {
1427 $prop = '_' . $this->fields[$field]['propname'];
1428 if (
1429 ($field === 'bool_admin_adh'
1430 || $field === 'bool_exempt_adh'
1431 || $field === 'bool_display_info'
1432 || $field === 'activite_adh')
1433 && $this->$prop === false
1434 ) {
1435 //Handle booleans for postgres ; bugs #18899 and #19354
1436 $values[$field] = $this->zdb->isPostgres() ? 'false' : 0;
1437 } elseif ($field === 'parent_id') {
1438 //handle parents
1439 if ($this->_parent === null) {
1440 $values['parent_id'] = new Expression('NULL');
1441 } elseif ($this->parent instanceof Adherent) {
1442 $values['parent_id'] = $this->_parent->id;
1443 } else {
1444 $values['parent_id'] = $this->_parent;
1445 }
1446 } else {
1447 $values[$field] = $this->$prop;
1448 }
1449 }
1450 }
1451
1452 //an empty value will cause date to be set to 1901-01-01, a null
1453 //will result in 0000-00-00. We want a database NULL value here.
1454 if (!$this->_birthdate) {
1455 $values['ddn_adh'] = new Expression('NULL');
1456 }
1457 if (!$this->_due_date) {
1458 $values['date_echeance'] = new Expression('NULL');
1459 }
1460
1461 if ($this->_title instanceof Title) {
1462 $values['titre_adh'] = $this->_title->id;
1463 } else {
1464 $values['titre_adh'] = new Expression('NULL');
1465 }
1466
1467 if (!$this->_parent) {
1468 $values['parent_id'] = new Expression('NULL');
1469 }
1470
1471 //fields that cannot be null
1472 $notnull = [
1473 '_surname' => 'prenom_adh',
1474 '_nickname' => 'pseudo_adh',
1475 '_address' => 'adresse_adh',
1476 '_zipcode' => 'cp_adh',
1477 '_town' => 'ville_adh'
1478 ];
1479 foreach ($notnull as $prop => $field) {
1480 if ($this->$prop === null) {
1481 $values[$field] = '';
1482 }
1483 }
1484
1485 $success = false;
1486 if (!isset($this->_id) || $this->_id == '') {
1487 //we're inserting a new member
1488 unset($values[self::PK]);
1489 //set modification date
1490 $this->_modification_date = date('Y-m-d');
1491 $values['date_modif_adh'] = $this->_modification_date;
1492
1493 $insert = $this->zdb->insert(self::TABLE);
1494 $insert->values($values);
1495 $add = $this->zdb->execute($insert);
1496 if ($add->count() > 0) {
1497 $this->_id = $this->zdb->getLastGeneratedValue($this);
1498 $this->_picture = new Picture($this->_id);
1499 // logging
1500 if ($this->_self_adh) {
1501 $hist->add(
1502 _T("Self_subscription as a member: ") .
1503 $this->getNameWithCase($this->_name, $this->_surname),
1504 $this->sname
1505 );
1506 } else {
1507 $hist->add(
1508 _T("Member card added"),
1509 $this->sname
1510 );
1511 }
1512 $success = true;
1513
1514 $event = 'member.add';
1515 } else {
1516 $hist->add(_T("Fail to add new member."));
1517 throw new \Exception(
1518 'An error occurred inserting new member!'
1519 );
1520 }
1521 } else {
1522 //we're editing an existing member
1523 if (!$this->isDueFree()) {
1524 // deadline
1525 $due_date = Contribution::getDueDate($this->zdb, $this->_id);
1526 if ($due_date) {
1527 $values['date_echeance'] = $due_date;
1528 }
1529 }
1530
1531 if (!$this->_password) {
1532 unset($values['mdp_adh']);
1533 }
1534
1535 $update = $this->zdb->update(self::TABLE);
1536 $update->set($values);
1537 $update->where(
1538 self::PK . '=' . $this->_id
1539 );
1540
1541 $edit = $this->zdb->execute($update);
1542
1543 //edit == 0 does not mean there were an error, but that there
1544 //were nothing to change
1545 if ($edit->count() > 0) {
1546 $this->updateModificationDate();
1547 $hist->add(
1548 _T("Member card updated"),
1549 $this->sname
1550 );
1551 }
1552 $success = true;
1553
1554 $event = 'member.edit';
1555 }
1556
1557 //dynamic fields
1558 if ($success) {
1559 $success = $this->dynamicsStore();
1560 }
1561
1562 //send event at the end of process, once all has been stored
1563 if ($event !== null) {
1564 $emitter->emit($event, $this);
1565 }
1566 return $success;
1567 } catch (Throwable $e) {
1568 Analog::log(
1569 'Something went wrong :\'( | ' . $e->getMessage() . "\n" .
1570 $e->getTraceAsString(),
1571 Analog::ERROR
1572 );
1573 return false;
1574 }
1575 }
1576
1577 /**
1578 * Update member modification date
1579 *
1580 * @return void
1581 */
1582 private function updateModificationDate()
1583 {
1584 try {
1585 $modif_date = date('Y-m-d');
1586 $update = $this->zdb->update(self::TABLE);
1587 $update->set(
1588 array('date_modif_adh' => $modif_date)
1589 )->where(self::PK . '=' . $this->_id);
1590
1591 $edit = $this->zdb->execute($update);
1592 $this->_modification_date = $modif_date;
1593 } catch (Throwable $e) {
1594 Analog::log(
1595 'Something went wrong updating modif date :\'( | ' .
1596 $e->getMessage() . "\n" . $e->getTraceAsString(),
1597 Analog::ERROR
1598 );
1599 }
1600 }
1601
1602 /**
1603 * Global getter method
1604 *
1605 * @param string $name name of the property we want to retrive
1606 *
1607 * @return false|object the called property
1608 */
1609 public function __get($name)
1610 {
1611 $forbidden = array(
1612 'admin', 'staff', 'due_free', 'appears_in_list', 'active',
1613 'row_classes', 'oldness', 'duplicate'
1614 );
1615 if (!defined('GALETTE_TESTS')) {
1616 $forbidden[] = 'password'; //keep that for tests only
1617 }
1618
1619 $virtuals = array(
1620 'sadmin', 'sstaff', 'sdue_free', 'sappears_in_list', 'sactive',
1621 'stitle', 'sstatus', 'sfullname', 'sname', 'saddress',
1622 'rbirthdate', 'sgender', 'contribstatus'
1623 );
1624
1625 if (in_array($name, $forbidden)) {
1626 switch ($name) {
1627 case 'admin':
1628 return $this->isAdmin();
1629 case 'staff':
1630 return $this->isStaff();
1631 case 'due_free':
1632 return $this->isDueFree();
1633 case 'appears_in_list':
1634 return $this->appearsInMembersList();
1635 case 'active':
1636 return $this->isActive();
1637 case 'duplicate':
1638 return $this->isDuplicate();
1639 default:
1640 throw new \RuntimeException("Call to __get for '$name' is forbidden!");
1641 }
1642 } else {
1643 if (in_array($name, $virtuals)) {
1644 if (substr($name, 0, 1) !== '_') {
1645 $real = '_' . substr($name, 1);
1646 } else {
1647 $real = $name;
1648 }
1649 switch ($name) {
1650 case 'sadmin':
1651 case 'sdue_free':
1652 case 'sappears_in_list':
1653 case 'sstaff':
1654 return (($this->$real) ? _T("Yes") : _T("No"));
1655 break;
1656 case 'sactive':
1657 return (($this->$real) ? _T("Active") : _T("Inactive"));
1658 break;
1659 case 'stitle':
1660 if (isset($this->_title) && $this->_title instanceof Title) {
1661 return $this->_title->tshort;
1662 } else {
1663 return null;
1664 }
1665 break;
1666 case 'sstatus':
1667 $status = new Status($this->zdb);
1668 return $status->getLabel($this->_status);
1669 break;
1670 case 'sfullname':
1671 return $this->getNameWithCase(
1672 $this->_name,
1673 $this->_surname,
1674 (isset($this->_title) ? $this->title : false)
1675 );
1676 break;
1677 case 'saddress':
1678 $address = $this->_address;
1679 if ($this->_address_continuation !== '' && $this->_address_continuation !== null) {
1680 $address .= "\n" . $this->_address_continuation;
1681 }
1682 return htmlspecialchars($address, ENT_QUOTES);
1683 break;
1684 case 'sname':
1685 return $this->getNameWithCase($this->_name, $this->_surname);
1686 break;
1687 case 'rbirthdate':
1688 return $this->_birthdate;
1689 break;
1690 case 'sgender':
1691 switch ($this->gender) {
1692 case self::MAN:
1693 return _T('Man');
1694 case self::WOMAN:
1695 return _T('Woman');
1696 default:
1697 return _T('Unspecified');
1698 }
1699 break;
1700 case 'contribstatus':
1701 return $this->getDues();
1702 break;
1703 }
1704 } else {
1705 if (substr($name, 0, 1) !== '_') {
1706 $rname = '_' . $name;
1707 } else {
1708 $rname = $name;
1709 }
1710
1711 switch ($name) {
1712 case 'id':
1713 case 'id_statut':
1714 if ($this->$rname !== null) {
1715 return (int)$this->$rname;
1716 } else {
1717 return null;
1718 }
1719 break;
1720 case 'birthdate':
1721 case 'creation_date':
1722 case 'modification_date':
1723 case 'due_date':
1724 if ($this->$rname != '') {
1725 try {
1726 $d = new \DateTime($this->$rname);
1727 return $d->format(__("Y-m-d"));
1728 } catch (Throwable $e) {
1729 //oops, we've got a bad date :/
1730 Analog::log(
1731 'Bad date (' . $this->$rname . ') | ' .
1732 $e->getMessage(),
1733 Analog::INFO
1734 );
1735 return $this->$rname;
1736 }
1737 }
1738 break;
1739 default:
1740 if (!property_exists($this, $rname)) {
1741 Analog::log(
1742 "Unknown property '$rname'",
1743 Analog::WARNING
1744 );
1745 return null;
1746 } else {
1747 return $this->$rname;
1748 }
1749 break;
1750 }
1751 }
1752 }
1753 }
1754
1755 /**
1756 * Get member email
1757 * If member does not have an email address, but is attached to
1758 * another member, we'll take information from its parent.
1759 *
1760 * @return string
1761 */
1762 public function getEmail()
1763 {
1764 $email = $this->_email;
1765 if (empty($email)) {
1766 $this->loadParent();
1767 $email = $this->parent->email;
1768 }
1769
1770 return $email;
1771 }
1772
1773 /**
1774 * Get member address.
1775 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1776 *
1777 * @return string
1778 */
1779 public function getAddress()
1780 {
1781 $address = $this->_address;
1782 if (empty($address) && $this->hasParent()) {
1783 $this->loadParent();
1784 $address = $this->parent->address;
1785 }
1786
1787 return $address;
1788 }
1789
1790 /**
1791 * Get member address continuation.
1792 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1793 *
1794 * @return string
1795 */
1796 public function getAddressContinuation()
1797 {
1798 $address = $this->_address;
1799 $address_continuation = $this->_address_continuation;
1800 if (empty($address) && $this->hasParent()) {
1801 $this->loadParent();
1802 $address_continuation = $this->parent->address_continuation;
1803 }
1804
1805 return $address_continuation;
1806 }
1807
1808 /**
1809 * Get member zipcode.
1810 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1811 *
1812 * @return string
1813 */
1814 public function getZipcode()
1815 {
1816 $address = $this->_address;
1817 $zip = $this->_zipcode;
1818 if (empty($address) && $this->hasParent()) {
1819 $this->loadParent();
1820 $zip = $this->parent->zipcode;
1821 }
1822
1823 return $zip;
1824 }
1825
1826 /**
1827 * Get member town.
1828 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1829 *
1830 * @return string
1831 */
1832 public function getTown()
1833 {
1834 $address = $this->_address;
1835 $town = $this->_town;
1836 if (empty($address) && $this->hasParent()) {
1837 $this->loadParent();
1838 $town = $this->parent->town;
1839 }
1840
1841 return $town;
1842 }
1843
1844 /**
1845 * Get member country.
1846 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1847 *
1848 * @return string
1849 */
1850 public function getCountry()
1851 {
1852 $address = $this->_address;
1853 $country = $this->_country;
1854 if (empty($address) && $this->hasParent()) {
1855 $this->loadParent();
1856 $country = $this->parent->country;
1857 }
1858
1859 return $country;
1860 }
1861
1862 /**
1863 * Get member age
1864 *
1865 * @return string
1866 */
1867 public function getAge()
1868 {
1869 if ($this->_birthdate == null) {
1870 return '';
1871 }
1872
1873 $d = \DateTime::createFromFormat('Y-m-d', $this->_birthdate);
1874 if ($d === false) {
1875 Analog::log(
1876 'Invalid birthdate: ' . $this->_birthdate,
1877 Analog::ERROR
1878 );
1879 return;
1880 }
1881
1882 return str_replace(
1883 '%age',
1884 $d->diff(new \DateTime())->y,
1885 _T(' (%age years old)')
1886 );
1887 }
1888
1889 /**
1890 * Get parent inherited fields
1891 *
1892 * @return array
1893 */
1894 public function getParentFields()
1895 {
1896 return $this->parent_fields;
1897 }
1898
1899 /**
1900 * Handle files (photo and dynamics files
1901 *
1902 * @param array $files Files sent
1903 *
1904 * @return array|true
1905 */
1906 public function handleFiles($files)
1907 {
1908 $this->errors = [];
1909 // picture upload
1910 if (isset($files['photo'])) {
1911 if ($files['photo']['error'] === UPLOAD_ERR_OK) {
1912 if ($files['photo']['tmp_name'] != '') {
1913 if (is_uploaded_file($files['photo']['tmp_name'])) {
1914 $res = $this->picture->store($files['photo']);
1915 if ($res < 0) {
1916 $this->errors[]
1917 = $this->picture->getErrorMessage($res);
1918 }
1919 }
1920 }
1921 } elseif ($files['photo']['error'] !== UPLOAD_ERR_NO_FILE) {
1922 Analog::log(
1923 $this->picture->getPhpErrorMessage($files['photo']['error']),
1924 Analog::WARNING
1925 );
1926 $this->errors[] = $this->picture->getPhpErrorMessage(
1927 $files['photo']['error']
1928 );
1929 }
1930 }
1931 $this->dynamicsFiles($_FILES);
1932
1933 if (count($this->errors) > 0) {
1934 Analog::log(
1935 'Some errors has been throwed attempting to edit/store a member files' . "\n" .
1936 print_r($this->errors, true),
1937 Analog::ERROR
1938 );
1939 return $this->errors;
1940 } else {
1941 return true;
1942 }
1943 }
1944
1945 /**
1946 * Set member as duplicate
1947 *
1948 * @return void
1949 */
1950 public function setDuplicate()
1951 {
1952 //mark as duplicated
1953 $this->_duplicate = true;
1954 $infos = $this->_others_infos_admin;
1955 $this->_others_infos_admin = str_replace(
1956 ['%name', '%id'],
1957 [$this->sname, $this->_id],
1958 _T('Duplicated from %name (%id)')
1959 );
1960 if (!empty($infos)) {
1961 $this->_others_infos_admin .= "\n" . $infos;
1962 }
1963 //drop id_adh
1964 $this->_id = null;
1965 //drop email, must be unique
1966 $this->_email = null;
1967 //drop creation date
1968 $this->_creation_date = date("Y-m-d");
1969 //drop login
1970 $this->_login = null;
1971 //reset picture
1972 $this->_picture = new Picture();
1973 //remove birthdate
1974 $this->_birthdate = null;
1975 //remove surname
1976 $this->_surname = null;
1977 //not admin
1978 $this->_admin = false;
1979 //not due free
1980 $this->_due_free = false;
1981 }
1982
1983 /**
1984 * Get current errors
1985 *
1986 * @return array
1987 */
1988 public function getErrors()
1989 {
1990 return $this->errors;
1991 }
1992
1993 /**
1994 * Get user groups
1995 *
1996 * @return array
1997 */
1998 public function getGroups()
1999 {
2000 return $this->_groups;
2001 }
2002
2003 /**
2004 * Get user managed groups
2005 *
2006 * @return array
2007 */
2008 public function getManagedGroups()
2009 {
2010 return $this->_managed_groups;
2011 }
2012
2013 /**
2014 * Can current logged in user edit member
2015 *
2016 * @param Login $login Login instance
2017 *
2018 * @return boolean
2019 */
2020 public function canEdit($login)
2021 {
2022 if ($this->id && $login->id == $this->id || $login->isAdmin() || $login->isStaff()) {
2023 return true;
2024 }
2025
2026 //check if requested member is part of managed groups
2027 if ($login->isGroupManager()) {
2028 foreach ($this->getGroups() as $g) {
2029 if ($login->isGroupManager($g->getId())) {
2030 return true;
2031 }
2032 }
2033 }
2034
2035 return false;
2036 }
2037
2038 /**
2039 * Are we currently duplicated a member?
2040 *
2041 * @return boolean
2042 */
2043 public function isDuplicate()
2044 {
2045 return $this->_duplicate;
2046 }
2047
2048 /**
2049 * Flag creation mail sending
2050 *
2051 * @param boolean $send True (default) to send creation email
2052 *
2053 * @return Adherent
2054 */
2055 public function setSendmail($send = true)
2056 {
2057 $this->sendmail = $send;
2058 return $this;
2059 }
2060
2061 /**
2062 * Should we send administrative emails to member?
2063 *
2064 * @return boolean
2065 */
2066 public function sendEMail()
2067 {
2068 return $this->sendmail;
2069 }
2070 }