3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
6 * Member class for galette
10 * Copyright © 2009-2021 The Galette Team
12 * This file is part of Galette (http://galette.tuxfamily.org).
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.
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.
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/>.
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2009-2021 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.7dev - 2009-06-02
37 namespace Galette\Entity
;
41 use Laminas\Db\Sql\Expression
;
43 use Galette\Core\Picture
;
44 use Galette\Core\GaletteMail
;
45 use Galette\Core\Password
;
46 use Galette\Core\Preferences
;
47 use Galette\Core\History
;
48 use Galette\Repository\Groups
;
49 use Galette\Core\Login
;
50 use Galette\Repository\Members
;
53 * Member class for galette
58 * @author Johan Cwiklinski <johan@x-tnd.be>
59 * @copyright 2009-2021 The Galette Team
60 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
61 * @link http://galette.tuxfamily.org
62 * @since Available since 0.7dev - 02-06-2009
64 * @property integer $id
65 * @property integer|Title $title Either a title id or an instance of Title
66 * @property string $stitle Title label
67 * @property string company_name
68 * @property string $name
69 * @property string $surname
70 * @property string $nickname
71 * @property string $birthdate Localized birth date
72 * @property string $rbirthdate Raw birth date
73 * @property string $birth_place
74 * @property integer $gender
75 * @property string $sgender Gender label
76 * @property string $job
77 * @property string $language
78 * @property integer $status
79 * @property string $sstatus Status label
80 * @property string $address
81 * @property string $address_continuation
82 * @property string $zipcode
83 * @property string $town
84 * @property string $country
85 * @property string $phone
86 * @property string $gsm
87 * @property string $email
88 * @property string $website
89 * @property string $msn
90 * @property string $icq
91 * @property string $jabber
92 * @property string $gnupgid
93 * @property string $fingerprint
94 * @property string $login
95 * @property string $creation_date Localized creation date
96 * @property string $modification_date Localized modification date
97 * @property string $due_date Localized due date
98 * @property string $others_infos
99 * @property string $others_infos_admin
100 * @property Picture $picture
101 * @property array $groups
102 * @property array $managed_groups
103 * @property integer|Adherent $parent Parent id if parent dep is not loaded, Adherent instance otherwise
104 * @property array $children
105 * @property boolean $admin better to rely on isAdmin()
106 * @property boolean $staff better to rely on isStaff()
107 * @property boolean $due_free better to rely on isDueFree()
108 * @property boolean $appears_in_list better to rely on appearsInMembersList()
109 * @property boolean $active better to rely on isActive()
110 * @property boolean $duplicate better to rely on isDuplicate()
111 * @property string $sadmin yes/no
112 * @property string $sstaff yes/no
113 * @property string $sdue_free yes/no
114 * @property string $sappears_in_list yes/no
115 * @property string $sactive yes/no
116 * @property string $sfullname
117 * @property string $sname
118 * @property string $saddress Concatened address and continuation
119 * @property string $contribstatus State of member contributions
120 * @property string $days_remaining
126 public const TABLE
= 'adherents';
127 public const PK
= 'id_adh';
130 public const MAN
= 1;
131 public const WOMAN
= 2;
133 public const AFTER_ADD_DEFAULT
= 0;
134 public const AFTER_ADD_TRANS
= 1;
135 public const AFTER_ADD_NEW
= 2;
136 public const AFTER_ADD_SHOW
= 3;
137 public const AFTER_ADD_LIST
= 4;
138 public const AFTER_ADD_HOME
= 5;
143 private $_company_name;
148 private $_birth_place;
154 //Contact information
156 private $_address_continuation; /** TODO: remove */
164 private $_msn; /** TODO: remove */
165 private $_icq; /** TODO: remove */
166 private $_jabber; /** TODO: remove */
167 private $_gnupgid; /** TODO: remove */
168 private $_fingerprint; /** TODO: remove */
169 //Galette relative information
170 private $_appears_in_list;
176 private $_creation_date;
177 private $_modification_date;
179 private $_others_infos;
180 private $_others_infos_admin;
183 private $_days_remaining;
185 private $_managed_groups;
188 private $_duplicate = false;
190 private $_row_classes;
192 private $_self_adh = false;
193 private $_deps = array(
203 private $preferences;
207 private $parent_fields = [
215 private $errors = [];
217 private $sendmail = false;
220 * Default constructor
222 * @param Db $zdb Database instance
223 * @param mixed $args Either a ResultSet row, its id or its
224 * login or its email for to load s specific
225 * member, or null to just instanciate object
226 * @param false|array $deps Dependencies configuration, see Adherent::$_deps
228 public function __construct(Db
$zdb, $args = null, $deps = null)
234 if ($deps !== null) {
235 if (is_array($deps)) {
236 $this->_deps
= array_merge(
240 } elseif ($deps === false) {
242 $this->_deps
= array_fill_keys(
243 array_keys($this->_deps
),
248 '$deps shoud be an array, ' . gettype($deps) . ' given!',
254 if ($args == null ||
is_int($args)) {
255 if (is_int($args) && $args > 0) {
258 $this->_active
= true;
259 $this->_language
= $i18n->getID();
260 $this->_creation_date
= date("Y-m-d");
261 $this->_status
= $this->getDefaultStatus();
262 $this->_title
= null;
263 $this->_gender
= self
::NC
;
264 $gp = new Password($this->zdb
);
265 $this->_password
= $gp->makeRandomPassword();
266 $this->_picture
= new Picture();
267 $this->_admin
= false;
268 $this->_staff
= false;
269 $this->_due_free
= false;
270 $this->_appears_in_list
= false;
271 $this->_parent
= null;
273 if ($this->_deps
['dynamics'] === true) {
274 $this->loadDynamicFields();
277 } elseif (is_object($args)) {
278 $this->loadFromRS($args);
279 } elseif (is_string($args)) {
280 $this->loadFromLoginOrMail($args);
285 * Loads a member from its id
287 * @param int $id the identifiant for the member to load
289 * @return bool true if query succeed, false otherwise
291 public function load($id)
294 $select = $this->zdb
->select(self
::TABLE
, 'a');
297 array('b' => PREFIX_DB
. Status
::TABLE
),
298 'a.' . Status
::PK
. '=b.' . Status
::PK
,
299 array('priorite_statut')
300 )->where(array(self
::PK
=> $id));
302 $results = $this->zdb
->execute($select);
304 if ($results->count() === 0) {
308 $this->loadFromRS($results->current());
310 } catch (Throwable
$e) {
312 'Cannot load member form id `' . $id . '` | ' . $e->getMessage(),
320 * Loads a member from its login
322 * @param string $login login for the member to load
324 * @return bool true if query succeed, false otherwise
326 public function loadFromLoginOrMail($login)
329 $select = $this->zdb
->select(self
::TABLE
);
330 if (GaletteMail
::isValidEmail($login)) {
331 //we got a valid email address, use it
332 $select->where(array('email_adh' => $login));
334 ///we did not get an email address, consider using login
335 $select->where(array('login_adh' => $login));
338 $results = $this->zdb
->execute($select);
339 $result = $results->current();
341 $this->loadFromRS($result);
343 } catch (Throwable
$e) {
345 'Cannot load member form login `' . $login . '` | ' .
354 * Populate object from a resultset row
356 * @param ResultSet $r the resultset row
360 private function loadFromRS($r)
362 $this->_self_adh
= false;
363 $this->_id
= $r->id_adh
;
365 if ($r->titre_adh
!== null) {
366 $this->_title
= new Title((int)$r->titre_adh
);
368 $this->_company_name
= $r->societe_adh
;
369 $this->_name
= $r->nom_adh
;
370 $this->_surname
= $r->prenom_adh
;
371 $this->_nickname
= $r->pseudo_adh
;
372 if ($r->ddn_adh
!= '1901-01-01') {
373 $this->_birthdate
= $r->ddn_adh
;
375 $this->_birth_place
= $r->lieu_naissance
;
376 $this->_gender
= (int)$r->sexe_adh
;
377 $this->_job
= $r->prof_adh
;
378 $this->_language
= $r->pref_lang
;
379 $this->_active
= ($r->activite_adh
== 1) ?
true : false;
380 $this->_status
= (int)$r->id_statut
;
381 //Contact information
382 $this->_address
= $r->adresse_adh
;
383 /** TODO: remove and merge with address */
384 $this->_address_continuation
= $r->adresse2_adh
;
385 $this->_zipcode
= $r->cp_adh
;
386 $this->_town
= $r->ville_adh
;
387 $this->_country
= $r->pays_adh
;
388 $this->_phone
= $r->tel_adh
;
389 $this->_gsm
= $r->gsm_adh
;
390 $this->_email
= $r->email_adh
;
391 $this->_website
= $r->url_adh
;
393 $this->_msn
= $r->msn_adh
;
395 $this->_icq
= $r->icq_adh
;
397 $this->_jabber
= $r->jabber_adh
;
399 $this->_gnupgid
= $r->gpgid
;
401 $this->_fingerprint
= $r->fingerprint
;
402 //Galette relative information
403 $this->_appears_in_list
= ($r->bool_display_info
== 1) ?
true : false;
404 $this->_admin
= ($r->bool_admin_adh
== 1) ?
true : false;
406 isset($r->priorite_statut
)
407 && $r->priorite_statut
< Members
::NON_STAFF_MEMBERS
409 $this->_staff
= true;
411 $this->_due_free
= ($r->bool_exempt_adh
== 1) ?
true : false;
412 $this->_login
= $r->login_adh
;
413 $this->_password
= $r->mdp_adh
;
414 $this->_creation_date
= $r->date_crea_adh
;
415 if ($r->date_modif_adh
!= '1901-01-01') {
416 $this->_modification_date
= $r->date_modif_adh
;
418 $this->_modification_date
= $this->_creation_date
;
420 $this->_due_date
= $r->date_echeance
;
421 $this->_others_infos
= $r->info_public_adh
;
422 $this->_others_infos_admin
= $r->info_adh
;
424 if ($r->parent_id
!== null) {
425 $this->_parent
= (int)$r->parent_id
;
426 if ($this->_deps
['parent'] === true) {
431 if ($this->_deps
['children'] === true) {
432 $this->loadChildren();
435 if ($this->_deps
['picture'] === true) {
436 $this->_picture
= new Picture($this->_id
);
439 if ($this->_deps
['groups'] === true) {
443 if ($this->_deps
['dues'] === true) {
447 if ($this->_deps
['dynamics'] === true) {
448 $this->loadDynamicFields();
457 private function loadParent()
459 if (!$this->_parent
instanceof Adherent
) {
460 $deps = array_fill_keys(array_keys($this->_deps
), false);
461 $this->_parent
= new Adherent($this->zdb
, (int)$this->_parent
, $deps);
466 * Load member children
470 private function loadChildren()
472 $this->_children
= array();
475 $select = $this->zdb
->select(self
::TABLE
);
479 'parent_id = ' . $this->_id
482 $results = $this->zdb
->execute($select);
484 if ($results->count() > 0) {
485 foreach ($results as $row) {
486 $deps = $this->_deps
;
487 $deps['children'] = false;
488 $deps['parent'] = false;
489 $this->_children
[] = new Adherent($this->zdb
, (int)$row->$id, $deps);
492 } catch (Throwable
$e) {
494 'Cannot load children for member #' . $this->_id
. ' | ' .
507 public function loadGroups()
509 $this->_groups
= Groups
::loadGroups($this->_id
);
510 $this->_managed_groups
= Groups
::loadManagedGroups($this->_id
);
514 * Retrieve status from preferences
516 * @return pref_statut
519 private function getDefaultStatus()
522 if ($preferences->pref_statut
!= '') {
523 return $preferences->pref_statut
;
526 'Unable to get pref_statut; is it defined in preferences?',
529 return Status
::DEFAULT_STATUS
;
534 * Check for dues status
538 private function checkDues()
540 //how many days since our beloved member has been created
541 $date_now = new \
DateTime();
542 $this->_oldness
= $date_now->diff(
543 new \
DateTime($this->_creation_date
)
546 if ($this->isDueFree()) {
547 //no fee required, we don't care about dates
548 $this->_row_classes
.= ' cotis-exempt';
550 //ok, fee is required. Let's check the dates
551 if ($this->_due_date
== '') {
552 $this->_row_classes
.= ' cotis-never';
554 $date_end = new \
DateTime($this->_due_date
);
555 $date_diff = $date_now->diff($date_end);
556 $this->_days_remaining
= ($date_diff->invert
== 1)
557 ?
$date_diff->days
* -1
560 if ($this->_days_remaining
== 0) {
561 $this->_row_classes
.= ' cotis-lastday';
562 } elseif ($this->_days_remaining
< 0) {
563 //check if member is still active
564 $this->_row_classes
.= $this->isActive() ?
' cotis-late' : ' cotis-old';
565 } elseif ($this->_days_remaining
< 30) {
566 $this->_row_classes
.= ' cotis-soon';
568 $this->_row_classes
.= ' cotis-ok';
579 public function isAdmin()
581 return $this->_admin
;
585 * Is user member of staff?
589 public function isStaff()
591 return $this->_staff
;
595 * Is member freed of dues?
599 public function isDueFree()
601 return $this->_due_free
;
605 * Is member in specified group?
607 * @param string $group_name Group name
611 public function isGroupMember($group_name)
613 if (is_array($this->_groups
)) {
614 foreach ($this->_groups
as $g) {
615 if ($g->getName() == $group_name) {
622 'Calling ' . __METHOD__
. ' without groups loaded!',
630 * Is member manager of specified group?
632 * @param string $group_name Group name
636 public function isGroupManager($group_name)
638 if (is_array($this->_managed_groups
)) {
639 foreach ($this->_managed_groups
as $mg) {
640 if ($mg->getName() == $group_name) {
647 'Calling ' . __METHOD__
. ' without groups loaded!',
655 * Does current member represents a company?
659 public function isCompany()
661 return trim($this->_company_name ??
'') != '';
665 * Is current member a man?
669 public function isMan()
671 return (int)$this->_gender
=== self
::MAN
;
675 * Is current member a woman?
679 public function isWoman()
681 return (int)$this->_gender
=== self
::WOMAN
;
686 * Can member appears in public members list?
690 public function appearsInMembersList()
692 return $this->_appears_in_list
;
700 public function isActive()
702 return $this->_active
;
706 * Does member have uploaded a picture?
710 public function hasPicture()
712 return $this->_picture
->hasPicture();
716 * Does member have a parent?
720 public function hasParent()
722 return !empty($this->_parent
);
726 * Does member have children?
730 public function hasChildren()
732 if ($this->_children
=== null) {
735 'Children has not been loaded!',
741 return count($this->_children
) > 0;
746 * Get row class related to current fee status
748 * @param boolean $public we want the class for public pages
750 * @return string the class to apply
752 public function getRowClass($public = false)
754 $strclass = ($this->isActive()) ?
'active' : 'inactive';
755 if ($public === false) {
756 $strclass .= $this->_row_classes
;
762 * Get current member due status
764 * @return string i18n string representing state of due
766 public function getDues()
769 $date_now = new \
DateTime();
770 $ddate = new \
DateTime($this->_due_date
);
771 $date_diff = $date_now->diff($ddate);
772 if ($this->isDueFree()) {
773 $ret = _T("Freed of dues");
774 } elseif ($this->_due_date
== '') {
775 $patterns = array('/%days/', '/%date/');
776 $cdate = new \
DateTime($this->_creation_date
);
779 $cdate->format(__("Y-m-d"))
781 if ($this->_active
) {
785 _T("Never contributed: Registered %days days ago (since %date)")
788 $ret = _T("Never contributed");
790 } elseif ($this->_days_remaining
== 0) {
791 if ($date_diff->invert
== 0) {
792 $ret = _T("Last day!");
794 $ret = _T("Late since today!");
796 } elseif ($this->_days_remaining
< 0) {
797 $patterns = array('/%days/', '/%date/');
799 $this->_days_remaining
* -1,
800 $ddate->format(__("Y-m-d"))
802 if ($this->_active
) {
806 _T("Late of %days days (since %date)")
809 $ret = _T("No longer member");
812 $patterns = array('/%days/', '/%date/');
814 $this->_days_remaining
,
815 $ddate->format(__("Y-m-d"))
820 _T("%days days remaining (ending on %date)")
827 * Retrieve Full name and surname for the specified member id
829 * @param Db $zdb Database instance
830 * @param integer $id Member id
831 * @param boolean $wid Add member id
832 * @param boolean $wnick Add member nickname
834 * @return string formatted Name and Surname
836 public static function getSName($zdb, $id, $wid = false, $wnick = false)
839 $select = $zdb->select(self
::TABLE
);
840 $select->where(self
::PK
. ' = ' . $id);
842 $results = $zdb->execute($select);
843 $row = $results->current();
844 return self
::getNameWithCase(
848 ($wid === true ?
$row->id_adh
: false),
849 ($wnick === true ?
$row->pseudo_adh
: false)
851 } catch (Throwable
$e) {
853 'Cannot get formatted name for member form id `' . $id . '` | ' .
862 * Get member name with correct case
864 * @param string $name Member name
865 * @param string $surname Mmeber surname
866 * @param false|Title $title Member title to show or false
867 * @param false|integer $id Member id to display or false
868 * @param false|string $nick Member nickname to display or false
872 public static function getNameWithCase($name, $surname, $title = false, $id = false, $nick = false)
876 if ($title !== false && $title instanceof Title
) {
877 $str .= $title->tshort
. ' ';
880 $str .= mb_strtoupper($name ??
'', 'UTF-8') . ' ' .
881 ucwords(mb_strtolower($surname ??
'', 'UTF-8'), " \t\r\n\f\v-_|");
883 if ($id !== false ||
$nick !== false) {
886 if ($nick !== false) {
890 if ($nick !== false && !empty($nick)) {
895 if ($id !== false ||
$nick !== false) {
898 return strip_tags($str);
902 * Change password for a given user
904 * @param Db $zdb Database instance
905 * @param string $id_adh Member identifier
906 * @param string $pass New password
910 public static function updatePassword(Db
$zdb, $id_adh, $pass)
913 $cpass = password_hash($pass, PASSWORD_BCRYPT
);
915 $update = $zdb->update(self
::TABLE
);
917 array('mdp_adh' => $cpass)
918 )->where(self
::PK
. ' = ' . $id_adh);
919 $zdb->execute($update);
921 'Password for `' . $id_adh . '` has been updated.',
925 } catch (Throwable
$e) {
927 'An error occurred while updating password for `' . $id_adh .
928 '` | ' . $e->getMessage(),
938 * @param string $field Field name
942 private function getFieldLabel($field)
944 $label = $this->fields
[$field]['label'];
945 //remove trailing ':' and then nbsp (for french at least)
946 $label = trim(trim($label, ':'), ' ');
951 * Retrieve fields from database
953 * @param Db $zdb Database instance
957 public static function getDbFields(Db
$zdb)
959 $columns = $zdb->getColumns(self
::TABLE
);
961 foreach ($columns as $col) {
962 $fields[] = $col->getName();
968 * Mark as self membership
972 public function setSelfMembership()
974 $this->_self_adh
= true;
978 * Is member up to date?
982 public function isUp2Date()
984 if ($this->_deps
['dues']) {
985 if ($this->isDueFree()) {
986 //member is due free, he's up to date.
989 //let's check from end date, if present
990 if ($this->_due_date
== null) {
993 $ech = new \
DateTime($this->_due_date
);
994 $now = new \
DateTime();
995 $now->setTime(0, 0, 0);
1000 throw new \
RuntimeException(
1001 'Cannot check if member is up to date, dues deps is disabled!'
1009 * @param Preferences $preferences Preferences instance
1010 * @param array $fields Members fields configuration
1011 * @param History $history History instance
1015 public function setDependencies(
1016 Preferences
$preferences,
1020 $this->preferences
= $preferences;
1021 $this->fields
= $fields;
1022 $this->history
= $history;
1026 * Check posted values validity
1028 * @param array $values All values to check, basically the $_POST array
1029 * after sending the form
1030 * @param array $required Array of required fields
1031 * @param array $disabled Array of disabled fields
1033 * @return true|array
1035 public function check($values, $required, $disabled)
1037 $this->errors
= array();
1040 foreach ($values as &$rawvalue) {
1041 if (is_string($rawvalue)) {
1042 $rawvalue = strip_tags($rawvalue);
1046 $fields = self
::getDbFields($this->zdb
);
1048 //reset company name if needeed
1049 if (!isset($values['is_company'])) {
1050 unset($values['is_company']);
1051 $values['societe_adh'] = '';
1054 //no parent if checkbox was unchecked
1056 !isset($values['attach'])
1057 && empty($this->_id
)
1058 && isset($values['parent_id'])
1060 unset($values['parent_id']);
1063 if (isset($values['duplicate'])) {
1064 //if we're duplicating, keep a trace (if an error occurs)
1065 $this->_duplicate
= true;
1068 foreach ($fields as $key) {
1069 //first of all, let's sanitize values
1070 $key = strtolower($key);
1071 $prop = '_' . $this->fields
[$key]['propname'];
1073 if (isset($values[$key])) {
1074 $value = $values[$key];
1075 if ($value !== true && $value !== false) {
1076 $value = trim($value);
1078 } elseif (empty($this->_id
)) {
1080 case 'bool_admin_adh':
1081 case 'bool_exempt_adh':
1082 case 'bool_display_info':
1085 case 'activite_adh':
1086 //values that are setted at object instanciation
1089 case 'date_crea_adh':
1095 //values that are setted at object instanciation
1096 $value = $this->$prop;
1103 //keep stored value on update
1104 if ($prop != '_password' ||
isset($values['mdp_adh']) && isset($values['mdp_adh2'])) {
1105 $value = $this->$prop;
1111 // if the field is enabled, check it
1112 if (!isset($disabled[$key])) {
1113 // fill up the adherent structure
1114 if ($value !== null && $value !== true && $value !== false && !is_object($value)) {
1115 $value = stripslashes($value);
1117 $this->$prop = $value;
1119 // now, check validity
1120 if ($value !== null && $value != '') {
1121 $this->validate($key, $value, $values);
1122 } elseif (empty($this->_id
)) {
1123 //ensure login and password are not empty
1124 if (($key == 'login_adh' ||
$key == 'mdp_adh') && !isset($required[$key])) {
1125 $p = new Password($this->zdb
);
1126 $generated_value = $p->makeRandomPassword(15);
1127 if ($key == 'login_adh') {
1128 //'@' is not permitted in logins
1129 $this->$prop = str_replace('@', 'a', $generated_value);
1131 $this->$prop = $generated_value;
1138 // missing required fields?
1139 foreach ($required as $key => $val) {
1140 $prop = '_' . $this->fields
[$key]['propname'];
1142 if (!isset($disabled[$key])) {
1143 $mandatory_missing = false;
1144 if (!isset($this->$prop) ||
$this->$prop == '') {
1145 $mandatory_missing = true;
1146 } elseif ($key === 'titre_adh' && $this->$prop == '-1') {
1147 $mandatory_missing = true;
1150 if ($mandatory_missing === true) {
1151 $this->errors
[] = str_replace(
1153 '<a href="#' . $key . '">' . $this->getFieldLabel($key) . '</a>',
1154 _T("- Mandatory field %field empty.")
1160 //attach to/detach from parent
1161 if (isset($values['detach_parent'])) {
1162 $this->_parent
= null;
1165 $this->dynamicsCheck($values, $required, $disabled);
1167 if (count($this->errors
) > 0) {
1169 'Some errors has been thew attempting to edit/store a member' . "\n" .
1170 print_r($this->errors
, true),
1173 return $this->errors
;
1178 'Member checked successfully.',
1186 * Validate data for given key
1187 * Set valid data in current object, also resets errors list
1189 * @param string $field Field name
1190 * @param mixed $value Value we want to set
1191 * @param array $values All values, for some references
1195 public function validate($field, $value, $values)
1197 global $preferences;
1199 $prop = '_' . $this->fields
[$field]['propname'];
1201 if ($value === null ||
(is_string($value) && trim($value) == '')) {
1202 //empty values are OK
1203 $this->$prop = $value;
1209 case 'date_crea_adh':
1210 case 'date_modif_adh_':
1212 case 'date_echeance':
1214 $d = \DateTime
::createFromFormat(__("Y-m-d"), $value);
1216 //try with non localized date
1217 $d = \DateTime
::createFromFormat("Y-m-d", $value);
1219 throw new \
Exception('Incorrect format');
1223 if ($field === 'ddn_adh') {
1224 $now = new \
DateTime();
1225 $now->setTime(0, 0, 0);
1226 $d->setTime(0, 0, 0);
1228 $diff = $now->diff($d);
1229 $days = (int)$diff->format('%R%a');
1231 $this->errors
[] = _T('- Birthdate must be set in the past!');
1234 $years = (int)$diff->format('%R%Y');
1235 if ($years <= -200) {
1236 $this->errors
[] = str_replace(
1239 _T('- Members must be less than 200 years old (currently %years)!')
1243 $this->$prop = $d->format('Y-m-d');
1244 } catch (Throwable
$e) {
1246 'Wrong date format. field: ' . $field .
1247 ', value: ' . $value . ', expected fmt: ' .
1248 __("Y-m-d") . ' | ' . $e->getMessage(),
1251 $this->errors
[] = str_replace(
1258 $this->getFieldLabel($field)
1260 _T("- Wrong date format (%date_format) for %field!")
1265 if ($value !== null && $value !== '') {
1266 if ($value == '-1') {
1267 $this->$prop = null;
1268 } elseif (!$value instanceof Title
) {
1269 $this->$prop = new Title((int)$value);
1272 $this->$prop = null;
1277 if (!GaletteMail
::isValidEmail($value)) {
1278 $this->errors
[] = _T("- Non-valid E-Mail address!") .
1279 ' (' . $this->getFieldLabel($field) . ')';
1281 if ($field == 'email_adh') {
1283 $select = $this->zdb
->select(self
::TABLE
);
1286 )->where(array('email_adh' => $value));
1287 if (!empty($this->_id
)) {
1289 self
::PK
. ' != ' . $this->_id
1293 $results = $this->zdb
->execute($select);
1294 if ($results->count() !== 0) {
1295 $this->errors
[] = _T("- This E-Mail address is already used by another member!");
1297 } catch (Throwable
$e) {
1299 'An error occurred checking member email unicity.',
1302 $this->errors
[] = _T("An error has occurred while looking if login already exists.");
1307 if ($value == 'http://') {
1309 } elseif (!isValidWebUrl($value)) {
1310 $this->errors
[] = _T("- Non-valid Website address! Maybe you've skipped the http://?");
1314 /** FIXME: add a preference for login lenght */
1315 if (strlen($value) < 2) {
1316 $this->errors
[] = str_replace(
1319 _T("- The username must be composed of at least %i characters!")
1322 //check if login does not contain the @ character
1323 if (strpos($value, '@') != false) {
1324 $this->errors
[] = _T("- The username cannot contain the @ character");
1326 //check if login is already taken
1328 $select = $this->zdb
->select(self
::TABLE
);
1331 )->where(array('login_adh' => $value));
1332 if (!empty($this->_id
)) {
1334 self
::PK
. ' != ' . $this->_id
1338 $results = $this->zdb
->execute($select);
1340 $results->count() !== 0
1341 ||
$value == $preferences->pref_admin_login
1343 $this->errors
[] = _T("- This username is already in use, please choose another one!");
1345 } catch (Throwable
$e) {
1347 'An error occurred checking member login unicity.',
1350 $this->errors
[] = _T("An error has occurred while looking if login already exists.");
1357 $this->_self_adh
!== true
1358 && (!isset($values['mdp_adh2'])
1359 ||
$values['mdp_adh2'] != $value)
1361 $this->errors
[] = _T("- The passwords don't match!");
1363 $this->_self_adh
=== true
1364 && !crypt($value, $values['mdp_crypt']) == $values['mdp_crypt']
1366 $this->errors
[] = _T("Password misrepeated: ");
1368 $pinfos = password_get_info($value);
1369 //check if value is already a hash
1370 if ($pinfos['algo'] == 0) {
1371 $this->$prop = password_hash(
1376 $pwcheck = new \Galette\Util\
Password($preferences);
1377 $pwcheck->setAdherent($this);
1378 if (!$pwcheck->isValid($value)) {
1379 $this->errors
= array_merge(
1381 $pwcheck->getErrors()
1389 $this->$prop = (int)$value;
1390 //check if status exists
1391 $select = $this->zdb
->select(Status
::TABLE
);
1392 $select->where(Status
::PK
. '= ' . $value);
1394 $results = $this->zdb
->execute($select);
1395 $result = $results->current();
1397 $this->errors
[] = str_replace(
1400 _T("Status #%id does not exists in database.")
1404 } catch (Throwable
$e) {
1406 'An error occurred checking status existance: ' . $e->getMessage(),
1409 $this->errors
[] = _T("An error has occurred while looking if status does exists.");
1413 if (in_array($value, [self
::NC
, self
::MAN
, self
::WOMAN
])) {
1414 $this->$prop = (int)$value;
1416 $this->errors
[] = _T("Gender %gender does not exists!");
1420 $this->$prop = ($value instanceof Adherent
) ?
(int)$value->id
: (int)$value;
1421 $this->loadParent();
1431 public function store()
1433 global $hist, $emitter, $login;
1436 if (!$login->isAdmin() && !$login->isStaff() && !$login->isGroupManager() && $this->id
== '') {
1437 $this->_parent
= $login->id
;
1442 $fields = self
::getDbFields($this->zdb
);
1444 foreach ($fields as $field) {
1446 $field !== 'date_modif_adh'
1447 ||
empty($this->_id
)
1449 $prop = '_' . $this->fields
[$field]['propname'];
1451 ($field === 'bool_admin_adh'
1452 ||
$field === 'bool_exempt_adh'
1453 ||
$field === 'bool_display_info'
1454 ||
$field === 'activite_adh')
1455 && $this->$prop === false
1457 //Handle booleans for postgres ; bugs #18899 and #19354
1458 $values[$field] = $this->zdb
->isPostgres() ?
'false' : 0;
1459 } elseif ($field === 'parent_id') {
1461 if ($this->_parent
=== null) {
1462 $values['parent_id'] = new Expression('NULL');
1463 } elseif ($this->parent
instanceof Adherent
) {
1464 $values['parent_id'] = $this->_parent
->id
;
1466 $values['parent_id'] = $this->_parent
;
1469 $values[$field] = $this->$prop;
1474 //an empty value will cause date to be set to 1901-01-01, a null
1475 //will result in 0000-00-00. We want a database NULL value here.
1476 if (!$this->_birthdate
) {
1477 $values['ddn_adh'] = new Expression('NULL');
1479 if (!$this->_due_date
) {
1480 $values['date_echeance'] = new Expression('NULL');
1483 if ($this->_title
instanceof Title
) {
1484 $values['titre_adh'] = $this->_title
->id
;
1486 $values['titre_adh'] = new Expression('NULL');
1489 if (!$this->_parent
) {
1490 $values['parent_id'] = new Expression('NULL');
1493 //fields that cannot be null
1495 '_surname' => 'prenom_adh',
1496 '_nickname' => 'pseudo_adh',
1497 '_address' => 'adresse_adh',
1498 '_zipcode' => 'cp_adh',
1499 '_town' => 'ville_adh'
1501 foreach ($notnull as $prop => $field) {
1502 if ($this->$prop === null) {
1503 $values[$field] = '';
1508 if (empty($this->_id
)) {
1509 //we're inserting a new member
1510 unset($values[self
::PK
]);
1511 //set modification date
1512 $this->_modification_date
= date('Y-m-d');
1513 $values['date_modif_adh'] = $this->_modification_date
;
1515 $insert = $this->zdb
->insert(self
::TABLE
);
1516 $insert->values($values);
1517 $add = $this->zdb
->execute($insert);
1518 if ($add->count() > 0) {
1519 $this->_id
= $this->zdb
->getLastGeneratedValue($this);
1520 $this->_picture
= new Picture($this->_id
);
1522 if ($this->_self_adh
) {
1524 _T("Self_subscription as a member: ") .
1525 $this->getNameWithCase($this->_name
, $this->_surname
),
1530 _T("Member card added"),
1536 $event = 'member.add';
1538 $hist->add(_T("Fail to add new member."));
1539 throw new \
Exception(
1540 'An error occurred inserting new member!'
1544 //we're editing an existing member
1545 if (!$this->isDueFree()) {
1547 $due_date = Contribution
::getDueDate($this->zdb
, $this->_id
);
1549 $values['date_echeance'] = $due_date;
1553 if (!$this->_password
) {
1554 unset($values['mdp_adh']);
1557 $update = $this->zdb
->update(self
::TABLE
);
1558 $update->set($values);
1560 self
::PK
. '=' . $this->_id
1563 $edit = $this->zdb
->execute($update);
1565 //edit == 0 does not mean there were an error, but that there
1566 //were nothing to change
1567 if ($edit->count() > 0) {
1568 $this->updateModificationDate();
1570 _T("Member card updated"),
1575 $event = 'member.edit';
1580 $success = $this->dynamicsStore();
1583 //send event at the end of process, once all has been stored
1584 if ($event !== null) {
1585 $emitter->emit($event, $this);
1588 } catch (Throwable
$e) {
1590 'Something went wrong :\'( | ' . $e->getMessage() . "\n" .
1591 $e->getTraceAsString(),
1599 * Update member modification date
1603 private function updateModificationDate()
1606 $modif_date = date('Y-m-d');
1607 $update = $this->zdb
->update(self
::TABLE
);
1609 array('date_modif_adh' => $modif_date)
1610 )->where(self
::PK
. '=' . $this->_id
);
1612 $edit = $this->zdb
->execute($update);
1613 $this->_modification_date
= $modif_date;
1614 } catch (Throwable
$e) {
1616 'Something went wrong updating modif date :\'( | ' .
1617 $e->getMessage() . "\n" . $e->getTraceAsString(),
1624 * Global getter method
1626 * @param string $name name of the property we want to retrive
1628 * @return false|object the called property
1630 public function __get($name)
1633 'admin', 'staff', 'due_free', 'appears_in_list', 'active',
1634 'row_classes', 'oldness', 'duplicate'
1636 if (!defined('GALETTE_TESTS')) {
1637 $forbidden[] = 'password'; //keep that for tests only
1641 'sadmin', 'sstaff', 'sdue_free', 'sappears_in_list', 'sactive',
1642 'stitle', 'sstatus', 'sfullname', 'sname', 'saddress',
1643 'rbirthdate', 'sgender', 'contribstatus'
1646 if (in_array($name, $forbidden)) {
1649 return $this->isAdmin();
1651 return $this->isStaff();
1653 return $this->isDueFree();
1654 case 'appears_in_list':
1655 return $this->appearsInMembersList();
1657 return $this->isActive();
1659 return $this->isDuplicate();
1661 throw new \
RuntimeException("Call to __get for '$name' is forbidden!");
1664 if (in_array($name, $virtuals)) {
1665 if (substr($name, 0, 1) !== '_') {
1666 $real = '_' . substr($name, 1);
1673 case 'sappears_in_list':
1675 return (($this->$real) ?
_T("Yes") : _T("No"));
1678 return (($this->$real) ?
_T("Active") : _T("Inactive"));
1681 if (isset($this->_title
) && $this->_title
instanceof Title
) {
1682 return $this->_title
->tshort
;
1688 $status = new Status($this->zdb
);
1689 return $status->getLabel($this->_status
);
1692 return $this->getNameWithCase(
1695 (isset($this->_title
) ?
$this->title
: false)
1699 $address = $this->_address
;
1700 if ($this->_address_continuation
!== '' && $this->_address_continuation
!== null) {
1701 $address .= "\n" . $this->_address_continuation
;
1703 return htmlspecialchars($address, ENT_QUOTES
);
1706 return $this->getNameWithCase($this->_name
, $this->_surname
);
1709 return $this->_birthdate
;
1712 switch ($this->gender
) {
1718 return _T('Unspecified');
1721 case 'contribstatus':
1722 return $this->getDues();
1726 if (substr($name, 0, 1) !== '_') {
1727 $rname = '_' . $name;
1735 if ($this->$rname !== null) {
1736 return (int)$this->$rname;
1742 case 'address_continuation':
1743 return $this->$rname ??
'';
1746 case 'creation_date':
1747 case 'modification_date':
1749 if ($this->$rname != '') {
1751 $d = new \
DateTime($this->$rname);
1752 return $d->format(__("Y-m-d"));
1753 } catch (Throwable
$e) {
1754 //oops, we've got a bad date :/
1756 'Bad date (' . $this->$rname . ') | ' .
1760 return $this->$rname;
1765 return ($this->_parent
instanceof Adherent
) ?
(int)$this->_parent
->id
: (int)$this->_parent
;
1768 if (!property_exists($this, $rname)) {
1770 "Unknown property '$rname'",
1775 return $this->$rname;
1785 * If member does not have an email address, but is attached to
1786 * another member, we'll take information from its parent.
1790 public function getEmail()
1792 $email = $this->_email
;
1793 if (empty($email)) {
1794 $this->loadParent();
1795 $email = $this->parent
->email
;
1802 * Get member address.
1803 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1807 public function getAddress()
1809 $address = $this->_address
;
1810 if (empty($address) && $this->hasParent()) {
1811 $this->loadParent();
1812 $address = $this->parent
->address
;
1819 * Get member address continuation.
1820 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1824 public function getAddressContinuation()
1826 $address = $this->_address
;
1827 $address_continuation = $this->_address_continuation
;
1828 if (empty($address) && $this->hasParent()) {
1829 $this->loadParent();
1830 $address_continuation = $this->parent
->address_continuation
;
1833 return $address_continuation;
1837 * Get member zipcode.
1838 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1842 public function getZipcode()
1844 $address = $this->_address
;
1845 $zip = $this->_zipcode
;
1846 if (empty($address) && $this->hasParent()) {
1847 $this->loadParent();
1848 $zip = $this->parent
->zipcode
;
1856 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1860 public function getTown()
1862 $address = $this->_address
;
1863 $town = $this->_town
;
1864 if (empty($address) && $this->hasParent()) {
1865 $this->loadParent();
1866 $town = $this->parent
->town
;
1873 * Get member country.
1874 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1878 public function getCountry()
1880 $address = $this->_address
;
1881 $country = $this->_country
;
1882 if (empty($address) && $this->hasParent()) {
1883 $this->loadParent();
1884 $country = $this->parent
->country
;
1895 public function getAge()
1897 if ($this->_birthdate
== null) {
1901 $d = \DateTime
::createFromFormat('Y-m-d', $this->_birthdate
);
1904 'Invalid birthdate: ' . $this->_birthdate
,
1912 $d->diff(new \
DateTime())->y
,
1913 _T(' (%age years old)')
1918 * Get parent inherited fields
1922 public function getParentFields()
1924 return $this->parent_fields
;
1928 * Handle files (photo and dynamics files)
1930 * @param array $files Files sent
1932 * @return array|true
1934 public function handleFiles($files)
1938 if (isset($files['photo'])) {
1939 if ($files['photo']['error'] === UPLOAD_ERR_OK
) {
1940 if ($files['photo']['tmp_name'] != '') {
1941 if (is_uploaded_file($files['photo']['tmp_name'])) {
1942 $res = $this->picture
->store($files['photo']);
1945 = $this->picture
->getErrorMessage($res);
1949 } elseif ($files['photo']['error'] !== UPLOAD_ERR_NO_FILE
) {
1951 $this->picture
->getPhpErrorMessage($files['photo']['error']),
1954 $this->errors
[] = $this->picture
->getPhpErrorMessage(
1955 $files['photo']['error']
1959 $this->dynamicsFiles($files);
1961 if (count($this->errors
) > 0) {
1963 'Some errors has been thew attempting to edit/store a member files' . "\n" .
1964 print_r($this->errors
, true),
1967 return $this->errors
;
1974 * Set member as duplicate
1978 public function setDuplicate()
1980 //mark as duplicated
1981 $this->_duplicate
= true;
1982 $infos = $this->_others_infos_admin
;
1983 $this->_others_infos_admin
= str_replace(
1985 [$this->sname
, $this->_id
],
1986 _T('Duplicated from %name (%id)')
1988 if (!empty($infos)) {
1989 $this->_others_infos_admin
.= "\n" . $infos;
1993 //drop email, must be unique
1994 $this->_email
= null;
1995 //drop creation date
1996 $this->_creation_date
= date("Y-m-d");
1998 $this->_login
= null;
2000 $this->_picture
= new Picture();
2002 $this->_birthdate
= null;
2004 $this->_surname
= null;
2006 $this->_admin
= false;
2008 $this->_due_free
= false;
2012 * Get current errors
2016 public function getErrors()
2018 return $this->errors
;
2026 public function getGroups()
2028 return $this->_groups
;
2032 * Get user managed groups
2036 public function getManagedGroups()
2038 return $this->_managed_groups
;
2042 * Can current logged in user edit member
2044 * @param Login $login Login instance
2048 public function canEdit(Login
$login)
2050 //admin and staff users can edit, as well as member itself
2051 if ($this->id
&& $login->id
== $this->id ||
$login->isAdmin() ||
$login->isStaff()) {
2055 //parent can edit their child cards
2056 if ($this->hasParent() && $this->parent_id
=== $login->id
) {
2060 //group managers can edit members of groups they manage
2061 if ($login->isGroupManager()) {
2062 foreach ($this->getGroups() as $g) {
2063 if ($login->isGroupManager($g->getId())) {
2073 * Can current logged in user display member
2075 * @param Login $login Login instance
2079 public function canShow(Login
$login)
2081 return $this->canEdit($login);
2085 * Are we currently duplicated a member?
2089 public function isDuplicate()
2091 return $this->_duplicate
;
2095 * Flag creation mail sending
2097 * @param boolean $send True (default) to send creation email
2101 public function setSendmail($send = true)
2103 $this->sendmail
= $send;
2108 * Should we send administrative emails to member?
2112 public function sendEMail()
2114 return $this->sendmail
;