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