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