3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
6 * Member class for galette
10 * Copyright © 2009-2023 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-2023 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
;
40 use Galette\Events\GaletteEvent
;
41 use Galette\Features\Socials
;
44 use Laminas\Db\Sql\Expression
;
46 use Galette\Core\Picture
;
47 use Galette\Core\GaletteMail
;
48 use Galette\Core\Password
;
49 use Galette\Core\Preferences
;
50 use Galette\Core\History
;
51 use Galette\Repository\Groups
;
52 use Galette\Core\Login
;
53 use Galette\Repository\Members
;
54 use Galette\Features\Dynamics
;
57 * Member class for galette
62 * @author Johan Cwiklinski <johan@x-tnd.be>
63 * @copyright 2009-2023 The Galette Team
64 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
65 * @link http://galette.tuxfamily.org
66 * @since Available since 0.7dev - 02-06-2009
68 * @property integer $id
69 * @property integer|Title $title Either a title id or an instance of Title
70 * @property string $stitle Title label
71 * @property string $company_name
72 * @property string $name
73 * @property string $surname
74 * @property string $nickname
75 * @property string $birthdate Localized birthdate
76 * @property string $rbirthdate Raw birthdate
77 * @property string $birth_place
78 * @property integer $gender
79 * @property string $sgender Gender label
80 * @property string $job
81 * @property string $language
82 * @property integer $status
83 * @property string $sstatus Status label
84 * @property string $address
85 * @property string $zipcode
86 * @property string $town
87 * @property string $country
88 * @property string $phone
89 * @property string $gsm
90 * @property string $email
91 * @property string $gnupgid
92 * @property string $fingerprint
93 * @property string $login
94 * @property string $creation_date Localized creation date
95 * @property string $modification_date Localized modification date
96 * @property string $due_date Localized due date
97 * @property string $others_infos
98 * @property string $others_infos_admin
99 * @property Picture $picture
100 * @property array $groups
101 * @property array $managed_groups
102 * @property integer|Adherent $parent Parent id if parent dep is not loaded, Adherent instance otherwise
103 * @property array $children
104 * @property boolean $admin better to rely on isAdmin()
105 * @property boolean $staff better to rely on isStaff()
106 * @property boolean $due_free better to rely on isDueFree()
107 * @property boolean $appears_in_list better to rely on appearsInMembersList()
108 * @property boolean $active better to rely on isActive()
109 * @property boolean $duplicate better to rely on isDuplicate()
110 * @property string $sadmin yes/no
111 * @property string $sstaff yes/no
112 * @property string $sdue_free yes/no
113 * @property string $sappears_in_list yes/no
114 * @property string $sactive yes/no
115 * @property string $sfullname
116 * @property string $sname
117 * @property string $saddress
118 * @property string $contribstatus State of member contributions
119 * @property string $days_remaining
120 * @property-read integer $parent_id
121 * @property Social $social Social networks/Contact
122 * @property string $number Member number
123 * @property-read bool $self_adh
130 public const TABLE
= 'adherents';
131 public const PK
= 'id_adh';
134 public const MAN
= 1;
135 public const WOMAN
= 2;
137 public const AFTER_ADD_DEFAULT
= 0;
138 public const AFTER_ADD_TRANS
= 1;
139 public const AFTER_ADD_NEW
= 2;
140 public const AFTER_ADD_SHOW
= 3;
141 public const AFTER_ADD_LIST
= 4;
142 public const AFTER_ADD_HOME
= 5;
147 private $_company_name;
152 private $_birth_place;
158 //Contact information
167 private $_fingerprint;
168 //Galette relative information
169 private $_appears_in_list;
171 private $_staff = false;
175 private $_creation_date;
176 private $_modification_date;
178 private $_others_infos;
179 private $_others_infos_admin;
182 private $_days_remaining;
183 private $_groups = [];
184 private $_managed_groups = [];
186 private $_children = [];
187 private $_duplicate = false;
191 private $_row_classes;
193 private $_self_adh = false;
194 private $_deps = array(
205 private $preferences;
209 private $parent_fields = [
216 private $errors = [];
218 private $sendmail = false;
221 * Default constructor
223 * @param Db $zdb Database instance
224 * @param mixed $args Either a ResultSet row, its id or its
225 * login or its email for to load s specific
226 * member, or null to just instantiate object
227 * @param false|array|null $deps Dependencies configuration, see Adherent::$_deps
229 public function __construct(Db
$zdb, $args = null, $deps = null)
235 if ($deps !== null) {
236 if (is_array($deps)) {
237 $this->_deps
= array_merge(
241 } elseif ($deps === false) {
243 $this->_deps
= array_fill_keys(
244 array_keys($this->_deps
),
249 '$deps should be an array, ' . gettype($deps) . ' given!',
255 if ($args == null ||
is_int($args)) {
256 if (is_int($args) && $args > 0) {
259 $this->_active
= true;
260 $this->_language
= $i18n->getID();
261 $this->_creation_date
= date("Y-m-d");
262 $this->_status
= $this->getDefaultStatus();
263 $this->_title
= null;
264 $this->_gender
= self
::NC
;
265 $gp = new Password($this->zdb
);
266 $this->_password
= $gp->makeRandomPassword();
267 $this->_picture
= new Picture();
268 $this->_admin
= false;
269 $this->_staff
= false;
270 $this->_due_free
= false;
271 $this->_appears_in_list
= false;
272 $this->_parent
= null;
274 if ($this->_deps
['dynamics'] === true) {
275 $this->loadDynamicFields();
278 } elseif (is_object($args)) {
279 $this->loadFromRS($args);
280 } elseif (is_string($args)) {
281 $this->loadFromLoginOrMail($args);
286 * Loads a member from its id
288 * @param int $id the identifier for the member to load
290 * @return bool true if query succeed, false otherwise
292 public function load(int $id): bool
295 $select = $this->zdb
->select(self
::TABLE
, 'a');
298 array('b' => PREFIX_DB
. Status
::TABLE
),
299 'a.' . Status
::PK
. '=b.' . Status
::PK
,
300 array('priorite_statut')
301 )->where(array(self
::PK
=> $id));
303 $results = $this->zdb
->execute($select);
305 if ($results->count() === 0) {
309 /** @var ArrayObject $result */
310 $result = $results->current();
311 $this->loadFromRS($result);
313 } catch (Throwable
$e) {
315 'Cannot load member form id `' . $id . '` | ' . $e->getMessage(),
323 * Loads a member from its login
325 * @param string $login login for the member to load
329 public function loadFromLoginOrMail(string $login): bool
332 $select = $this->zdb
->select(self
::TABLE
);
333 if (GaletteMail
::isValidEmail($login)) {
334 //we got a valid email address, use it
335 $select->where(array('email_adh' => $login));
337 ///we did not get an email address, consider using login
338 $select->where(array('login_adh' => $login));
341 $results = $this->zdb
->execute($select);
342 if ($results->count() > 0) {
343 /** @var ArrayObject $result */
344 $result = $results->current();
345 $this->loadFromRS($result);
348 } catch (Throwable
$e) {
350 'Cannot load member form login `' . $login . '` | ' .
359 * Populate object from a resultset row
361 * @param ArrayObject $r the resultset row
365 private function loadFromRS(ArrayObject
$r): void
367 $this->_self_adh
= false;
368 $this->_id
= $r->id_adh
;
370 if ($r->titre_adh
!== null) {
371 $this->_title
= new Title((int)$r->titre_adh
);
373 $this->_company_name
= $r->societe_adh
;
374 $this->_name
= $r->nom_adh
;
375 $this->_surname
= $r->prenom_adh
;
376 $this->_nickname
= $r->pseudo_adh
;
377 if ($r->ddn_adh
!= '1901-01-01') {
378 $this->_birthdate
= $r->ddn_adh
;
380 $this->_birth_place
= $r->lieu_naissance
;
381 $this->_gender
= (int)$r->sexe_adh
;
382 $this->_job
= $r->prof_adh
;
383 $this->_language
= $r->pref_lang
;
384 $this->_active
= $r->activite_adh
== 1;
385 $this->_status
= (int)$r->id_statut
;
386 //Contact information
387 $this->_address
= $r->adresse_adh
;
388 $this->_zipcode
= $r->cp_adh
;
389 $this->_town
= $r->ville_adh
;
390 $this->_country
= $r->pays_adh
;
391 $this->_phone
= $r->tel_adh
;
392 $this->_gsm
= $r->gsm_adh
;
393 $this->_email
= $r->email_adh
;
394 $this->_gnupgid
= $r->gpgid
;
395 $this->_fingerprint
= $r->fingerprint
;
396 //Galette relative information
397 $this->_appears_in_list
= $r->bool_display_info
== 1;
398 $this->_admin
= $r->bool_admin_adh
== 1;
400 isset($r->priorite_statut
)
401 && $r->priorite_statut
< Members
::NON_STAFF_MEMBERS
403 $this->_staff
= true;
405 $this->_due_free
= $r->bool_exempt_adh
== 1;
406 $this->_login
= $r->login_adh
;
407 $this->_password
= $r->mdp_adh
;
408 $this->_creation_date
= $r->date_crea_adh
;
409 if ($r->date_modif_adh
!= '1901-01-01') {
410 $this->_modification_date
= $r->date_modif_adh
;
412 $this->_modification_date
= $this->_creation_date
;
414 $this->_due_date
= $r->date_echeance
;
415 $this->_others_infos
= $r->info_public_adh
;
416 $this->_others_infos_admin
= $r->info_adh
;
417 $this->_number
= $r->num_adh
;
419 if ($r->parent_id
!== null) {
420 $this->_parent
= (int)$r->parent_id
;
421 if ($this->_deps
['parent'] === true) {
426 if ($this->_deps
['children'] === true) {
427 $this->loadChildren();
430 if ($this->_deps
['picture'] === true) {
431 $this->_picture
= new Picture($this->_id
);
434 if ($this->_deps
['groups'] === true) {
438 if ($this->_deps
['dues'] === true) {
442 if ($this->_deps
['dynamics'] === true) {
443 $this->loadDynamicFields();
446 if ($this->_deps
['socials'] === true) {
447 $this->loadSocials();
456 private function loadParent(): void
458 if (!$this->_parent
instanceof Adherent
) {
459 $deps = array_fill_keys(array_keys($this->_deps
), false);
460 $this->_parent
= new Adherent($this->zdb
, (int)$this->_parent
, $deps);
465 * Load member children
469 private function loadChildren(): void
471 $this->_children
= array();
474 $select = $this->zdb
->select(self
::TABLE
);
477 )->where(['parent_id' => $this->_id
]);
479 $results = $this->zdb
->execute($select);
481 if ($results->count() > 0) {
482 foreach ($results as $row) {
483 $deps = $this->_deps
;
484 $deps['children'] = false;
485 $deps['parent'] = false;
486 $this->_children
[] = new Adherent($this->zdb
, (int)$row->$id, $deps);
489 } catch (Throwable
$e) {
491 'Cannot load children for member #' . $this->_id
. ' | ' .
504 public function loadGroups(): void
506 $this->_groups
= Groups
::loadGroups($this->_id
);
507 $this->_managed_groups
= Groups
::loadManagedGroups($this->_id
);
511 * Load member social network/contact information
515 public function loadSocials(): void
517 $this->_socials
= Social
::getListForMember($this->_id
);
521 * Retrieve status from preferences
526 private function getDefaultStatus(): int
529 if ($preferences->pref_statut
!= '') {
530 return $preferences->pref_statut
;
533 'Unable to get pref_statut; is it defined in preferences?',
536 return Status
::DEFAULT_STATUS
;
541 * Check for dues status
545 private function checkDues(): void
547 //how many days since our beloved member has been created
548 $now = new \
DateTime();
549 $this->_oldness
= $now->diff(
550 new \
DateTime($this->_creation_date
)
553 if ($this->isDueFree()) {
554 //no fee required, we don't care about dates
555 $this->_row_classes
.= ' cotis-exempt';
557 //ok, fee is required. Let's check the dates
558 if ($this->_due_date
== '') {
559 $this->_row_classes
.= ' cotis-never';
561 // To count the days remaining, the next begin date is required.
562 $due_date = new \
DateTime($this->_due_date
);
563 $next_begin_date = clone $due_date;
564 $next_begin_date->add(new \
DateInterval('P1D'));
565 $date_diff = $now->diff($next_begin_date);
566 $this->_days_remaining
= $date_diff->days
;
568 if ($date_diff->invert
== 0 && $date_diff->days
>= 0) {
569 $this->_days_remaining
= $date_diff->days
;
570 if ($this->_days_remaining
<= 30) {
571 if ($date_diff->days
== 0) {
572 $this->_row_classes
.= ' cotis-lastday';
574 $this->_row_classes
.= ' cotis-soon';
576 $this->_row_classes
.= ' cotis-ok';
579 } elseif ($date_diff->invert
== 1 && $date_diff->days
>= 0) {
580 $this->_days_remaining
= $date_diff->days
;
581 //check if member is still active
582 $this->_row_classes
.= $this->isActive() ?
' cotis-late' : ' cotis-old';
593 public function isAdmin(): bool
595 return $this->_admin
;
599 * Is user member of staff?
603 public function isStaff(): bool
605 return $this->_staff
;
609 * Is member freed of dues?
613 public function isDueFree(): bool
615 return $this->_due_free
;
619 * Is member in specified group?
621 * @param string $group_name Group name
625 public function isGroupMember(string $group_name): bool
627 if (!$this->isDepEnabled('groups')) {
631 foreach ($this->_groups
as $g) {
632 if ($g->getName() == $group_name) {
640 * Is member manager of specified group?
642 * @param string $group_name Group name
646 public function isGroupManager(string $group_name): bool
648 if (!$this->isDepEnabled('groups')) {
652 foreach ($this->_managed_groups
as $mg) {
653 if ($mg->getName() == $group_name) {
661 * Does current member represents a company?
665 public function isCompany(): bool
667 return trim($this->_company_name ??
'') != '';
671 * Is current member a man?
675 public function isMan(): bool
677 return (int)$this->_gender
=== self
::MAN
;
681 * Is current member a woman?
685 public function isWoman(): bool
687 return (int)$this->_gender
=== self
::WOMAN
;
692 * Can member appears in public members list?
696 public function appearsInMembersList(): bool
698 return $this->_appears_in_list
;
706 public function isActive(): bool
708 return $this->_active
;
712 * Does member have uploaded a picture?
716 public function hasPicture(): bool
718 return $this->_picture
->hasPicture();
722 * Does member have a parent?
726 public function hasParent(): bool
728 return !empty($this->_parent
);
732 * Does member have children?
736 public function hasChildren(): bool
738 if ($this->_children
=== null) {
741 'Children has not been loaded!',
747 return count($this->_children
) > 0;
752 * Get row class related to current fee status
754 * @param boolean $public we want the class for public pages
756 * @return string the class to apply
758 public function getRowClass(bool $public = false): string
760 $strclass = ($this->isActive()) ?
'active-account' : 'inactive-account';
761 if ($public === false) {
762 $strclass .= $this->_row_classes
;
768 * Get current member due status
770 * @return string i18n string representing state of due
772 public function getDues(): string
775 $never_contributed = false;
776 $now = new \
DateTime();
777 // To count the days remaining, the next begin date is required.
778 if ($this->_due_date
=== null) {
779 $this->_due_date
= $now->format('Y-m-d');
780 $never_contributed = true;
782 $due_date = new \
DateTime($this->_due_date
);
783 $next_begin_date = clone $due_date;
784 $next_begin_date->add(new \
DateInterval('P1D'));
785 $date_diff = $now->diff($next_begin_date);
786 if ($this->isDueFree()) {
787 $ret = _T("Freed of dues");
788 } elseif ($never_contributed === true) {
789 $patterns = array('/%days/', '/%date/');
790 $cdate = new \
DateTime($this->_creation_date
);
793 $cdate->format(__("Y-m-d"))
795 if ($this->_active
) {
799 _T("Never contributed: Registered %days days ago (since %date)")
802 $ret = _T("Never contributed");
804 // Last active or first expired day
805 } elseif ($this->_days_remaining
== 0) {
806 if ($date_diff->invert
== 0) {
807 $ret = _T("Last day!");
809 $ret = _T("Late since today!");
812 } elseif ($date_diff->invert
== 0 && $this->_days_remaining
> 0) {
813 $patterns = array('/%days/', '/%date/');
815 $this->_days_remaining
,
816 $due_date->format(__("Y-m-d"))
821 _T("%days days remaining (ending on %date)")
824 } elseif ($date_diff->invert
== 1 && $this->_days_remaining
> 0) {
825 $patterns = array('/%days/', '/%date/');
827 // We need the number of days expired, not the number of days remaining.
828 $this->_days_remaining +
1,
829 $due_date->format(__("Y-m-d"))
831 if ($this->_active
) {
835 _T("Late of %days days (since %date)")
838 $ret = _T("No longer member");
845 * Retrieve Full name and surname for the specified member id
847 * @param Db $zdb Database instance
848 * @param integer $id Member id
849 * @param boolean $wid Add member id
850 * @param boolean $wnick Add member nickname
852 * @return string formatted Name and Surname
854 public static function getSName(Db
$zdb, int $id, bool $wid = false, bool $wnick = false): string
857 $select = $zdb->select(self
::TABLE
);
858 $select->where([self
::PK
=> $id]);
860 $results = $zdb->execute($select);
861 $row = $results->current();
862 return self
::getNameWithCase(
866 ($wid === true ?
$row->id_adh
: false),
867 ($wnick === true ?
$row->pseudo_adh
: false)
869 } catch (Throwable
$e) {
871 'Cannot get formatted name for member form id `' . $id . '` | ' .
880 * Get member name with correct case
882 * @param string $name Member name
883 * @param string $surname Member surname
884 * @param false|Title $title Member title to show or false
885 * @param false|integer $id Member id to display or false
886 * @param false|string $nick Member nickname to display or false
890 public static function getNameWithCase(
899 if ($title instanceof Title
) {
900 $str .= $title->tshort
. ' ';
903 $str .= mb_strtoupper($name ??
'', 'UTF-8') . ' ' .
904 ucwords(mb_strtolower($surname ??
'', 'UTF-8'), " \t\r\n\f\v-_|");
906 if ($id !== false ||
!empty($nick)) {
918 if ($id !== false ||
!empty($nick)) {
921 return strip_tags($str);
925 * Change password for a given user
927 * @param Db $zdb Database instance
928 * @param integer $id_adh Member identifier
929 * @param string $pass New password
933 public static function updatePassword(Db
$zdb, int $id_adh, string $pass): bool
936 $cpass = password_hash($pass, PASSWORD_BCRYPT
);
938 $update = $zdb->update(self
::TABLE
);
940 array('mdp_adh' => $cpass)
941 )->where([self
::PK
=> $id_adh]);
942 $zdb->execute($update);
944 'Password for `' . $id_adh . '` has been updated.',
948 } catch (Throwable
$e) {
950 'An error occurred while updating password for `' . $id_adh .
951 '` | ' . $e->getMessage(),
961 * @param string $field Field name
965 private function getFieldLabel(string $field): string
967 $label = $this->fields
[$field]['label'] ??
'';
969 $label = str_replace(' ', ' ', $label);
970 //remove trailing ':' and then trim
971 $label = trim(trim($label, ':'));
976 * Retrieve fields from database
978 * @param Db $zdb Database instance
982 public static function getDbFields(Db
$zdb): array
984 $columns = $zdb->getColumns(self
::TABLE
);
986 foreach ($columns as $col) {
987 $fields[] = $col->getName();
993 * Mark as self membership
997 public function setSelfMembership(): void
999 $this->_self_adh
= true;
1003 * Is member up to date?
1007 public function isUp2Date(): bool
1009 if (!$this->isDepEnabled('dues')) {
1013 if ($this->isDueFree()) {
1014 //member is due free, he's up to date.
1017 //let's check from due date, if present
1018 if ($this->_due_date
== null) {
1021 $due_date = new \
DateTime($this->_due_date
);
1022 $now = new \
DateTime();
1023 $now->setTime(0, 0, 0);
1024 return $due_date >= $now;
1032 * @param Preferences $preferences Preferences instance
1033 * @param array $fields Members fields configuration
1034 * @param History $history History instance
1038 public function setDependencies(
1039 Preferences
$preferences,
1043 $this->preferences
= $preferences;
1044 $this->fields
= $fields;
1045 $this->history
= $history;
1049 * Check posted values validity
1051 * @param array $values All values to check, basically the $_POST array
1052 * after sending the form
1053 * @param array $required Array of required fields
1054 * @param array $disabled Array of disabled fields
1056 * @return true|array
1058 public function check(array $values, array $required, array $disabled)
1062 $this->errors
= array();
1065 foreach ($values as &$rawvalue) {
1066 if (is_string($rawvalue)) {
1067 $rawvalue = strip_tags($rawvalue);
1071 $fields = self
::getDbFields($this->zdb
);
1073 //reset company name if needed
1074 if (!isset($values['is_company'])) {
1075 unset($values['is_company']);
1076 $values['societe_adh'] = '';
1079 //no parent if checkbox was unchecked
1081 !isset($values['attach'])
1082 && empty($this->_id
)
1083 && isset($values['parent_id'])
1085 unset($values['parent_id']);
1088 if (isset($values['duplicate'])) {
1089 //if we're duplicating, keep a trace (if an error occurs)
1090 $this->_duplicate
= true;
1093 foreach ($fields as $key) {
1094 //first, let's sanitize values
1095 $key = strtolower($key);
1096 $prop = '_' . $this->fields
[$key]['propname'];
1098 if (isset($values[$key])) {
1099 $value = $values[$key];
1100 if ($value !== true && $value !== false) {
1101 //@phpstan-ignore-next-line
1102 $value = trim($value ??
'');
1104 } elseif (empty($this->_id
)) {
1106 case 'bool_admin_adh':
1107 case 'bool_exempt_adh':
1108 case 'bool_display_info':
1111 case 'activite_adh':
1112 //values that are set at object instantiation
1115 case 'date_crea_adh':
1121 //values that are set at object instantiation
1122 $value = $this->$prop;
1132 //keep stored value on update
1133 if ($prop != '_password' ||
isset($values['mdp_adh']) && isset($values['mdp_adh2'])) {
1134 $value = $this->$prop;
1140 // if the field is enabled, check it
1141 if (!isset($disabled[$key])) {
1142 // fill up the adherent structure
1143 if ($value !== null && $value !== true && $value !== false && !is_object($value)) {
1144 $value = stripslashes($value);
1146 $this->$prop = $value;
1148 // now, check validity
1149 if ($value !== null && $value != '') {
1150 if ($key !== 'mdp_adh') {
1151 $this->validate($key, $value, $values);
1153 } elseif (empty($this->_id
)) {
1154 //ensure login and password are not empty
1155 if (($key == 'login_adh' ||
$key == 'mdp_adh') && !isset($required[$key])) {
1156 $p = new Password($this->zdb
);
1157 $generated_value = $p->makeRandomPassword(15);
1158 if ($key == 'login_adh') {
1159 //'@' is not permitted in logins
1160 $this->$prop = str_replace('@', 'a', $generated_value);
1162 $this->$prop = $generated_value;
1169 //password checks need data to be previously set
1170 if (isset($values['mdp_adh'])) {
1171 $this->validate('mdp_adh', $values['mdp_adh'], $values);
1174 // missing required fields?
1175 foreach ($required as $key => $val) {
1176 $prop = '_' . $this->fields
[$key]['propname'];
1178 if (!isset($disabled[$key])) {
1179 $mandatory_missing = false;
1180 if (!isset($this->$prop) ||
$this->$prop == '') {
1181 $mandatory_missing = true;
1182 } elseif ($key === 'titre_adh' && $this->$prop == '-1') {
1183 $mandatory_missing = true;
1186 if ($mandatory_missing === true) {
1187 $this->errors
[] = str_replace(
1189 '<a href="#' . $key . '">' . $this->getFieldLabel($key) . '</a>',
1190 _T("- Mandatory field %field empty.")
1196 //attach to/detach from parent
1197 if (isset($values['detach_parent'])) {
1198 $this->_parent
= null;
1201 if ($login->isGroupManager() && !$login->isAdmin() && !$login->isStaff() && $this->parent_id
!== $login->id
) {
1202 if (!isset($values['groups_adh'])) {
1203 $this->errors
[] = _T('You have to select a group you own!');
1205 $owned_group = false;
1206 foreach ($values['groups_adh'] as $group) {
1207 list($gid) = explode('|', $group);
1208 if ($login->isGroupManager($gid)) {
1209 $owned_group = true;
1212 if ($owned_group === false) {
1213 $this->errors
[] = _T('You have to select a group you own!');
1218 $this->dynamicsCheck($values, $required, $disabled);
1219 $this->checkSocials($values);
1221 if (count($this->errors
) > 0) {
1223 'Some errors has been thew attempting to edit/store a member' . "\n" .
1224 print_r($this->errors
, true),
1227 return $this->errors
;
1232 'Member checked successfully.',
1240 * Validate data for given key
1241 * Set valid data in current object, also resets errors list
1243 * @param string $field Field name
1244 * @param mixed $value Value we want to set
1245 * @param array $values All values, for some references
1249 public function validate(string $field, $value, array $values): void
1251 global $preferences;
1253 $prop = '_' . $this->fields
[$field]['propname'];
1255 if ($value === null ||
(is_string($value) && trim($value) == '')) {
1256 //empty values are OK
1257 $this->$prop = $value;
1263 case 'date_crea_adh':
1264 case 'date_modif_adh_':
1266 case 'date_echeance':
1268 $d = \DateTime
::createFromFormat(__("Y-m-d"), $value);
1270 //try with non localized date
1271 $d = \DateTime
::createFromFormat("Y-m-d", $value);
1273 throw new \
Exception('Incorrect format');
1277 if ($field === 'ddn_adh') {
1278 $now = new \
DateTime();
1279 $now->setTime(0, 0, 0);
1280 $d->setTime(0, 0, 0);
1282 $diff = $now->diff($d);
1283 $days = (int)$diff->format('%R%a');
1285 $this->errors
[] = _T('- Birthdate must be set in the past!');
1288 $years = (int)$diff->format('%R%Y');
1289 if ($years <= -200) {
1290 $this->errors
[] = str_replace(
1293 _T('- Members must be less than 200 years old (currently %years)!')
1297 $this->$prop = $d->format('Y-m-d');
1298 } catch (Throwable
$e) {
1300 'Wrong date format. field: ' . $field .
1301 ', value: ' . $value . ', expected fmt: ' .
1302 __("Y-m-d") . ' | ' . $e->getMessage(),
1305 $this->errors
[] = str_replace(
1312 $this->getFieldLabel($field)
1314 _T("- Wrong date format (%date_format) for %field!")
1319 if ($value !== null && $value !== '') {
1320 if ($value == '-1') {
1321 $this->$prop = null;
1322 } elseif (!$value instanceof Title
) {
1323 $this->$prop = new Title((int)$value);
1326 $this->$prop = null;
1330 if (!GaletteMail
::isValidEmail($value)) {
1331 $this->errors
[] = _T("- Non-valid E-Mail address!") .
1332 ' (' . $this->getFieldLabel($field) . ')';
1334 if ($field == 'email_adh') {
1336 $select = $this->zdb
->select(self
::TABLE
);
1339 )->where(array('email_adh' => $value));
1340 if (!empty($this->_id
)) {
1341 $select->where
->notEqualTo(
1347 $results = $this->zdb
->execute($select);
1348 if ($results->count() !== 0) {
1349 $this->errors
[] = _T("- This E-Mail address is already used by another member!");
1351 } catch (Throwable
$e) {
1353 'An error occurred checking member email uniqueness.',
1356 $this->errors
[] = _T("An error has occurred while looking if login already exists.");
1361 /** FIXME: add a preference for login length */
1362 if (strlen($value) < 2) {
1363 $this->errors
[] = str_replace(
1366 _T("- The username must be composed of at least %i characters!")
1369 //check if login does not contain the @ character
1370 if (strpos($value, '@') != false) {
1371 $this->errors
[] = _T("- The username cannot contain the @ character");
1373 //check if login is already taken
1375 $select = $this->zdb
->select(self
::TABLE
);
1378 )->where(array('login_adh' => $value));
1379 if (!empty($this->_id
)) {
1380 $select->where
->notEqualTo(
1386 $results = $this->zdb
->execute($select);
1388 $results->count() !== 0
1389 ||
$value == $preferences->pref_admin_login
1391 $this->errors
[] = _T("- This username is already in use, please choose another one!");
1393 } catch (Throwable
$e) {
1395 'An error occurred checking member login uniqueness.',
1398 $this->errors
[] = _T("An error has occurred while looking if login already exists.");
1405 $this->_self_adh
!== true
1406 && (!isset($values['mdp_adh2'])
1407 ||
$values['mdp_adh2'] != $value)
1409 $this->errors
[] = _T("- The passwords don't match!");
1411 $this->_self_adh
=== true
1412 && !crypt($value, $values['mdp_crypt']) == $values['mdp_crypt']
1414 $this->errors
[] = _T("Password misrepeated: ");
1416 $pinfos = password_get_info($value);
1417 //check if value is already a hash
1418 if ($pinfos['algo'] == 0) {
1419 $this->$prop = password_hash(
1424 $pwcheck = new \Galette\Util\
Password($preferences);
1425 $pwcheck->setAdherent($this);
1426 if (!$pwcheck->isValid($value)) {
1427 $this->errors
= array_merge(
1429 $pwcheck->getErrors()
1437 $this->$prop = (int)$value;
1438 //check if status exists
1439 $select = $this->zdb
->select(Status
::TABLE
);
1440 $select->where([Status
::PK
=> $value]);
1442 $results = $this->zdb
->execute($select);
1443 $result = $results->current();
1445 $this->errors
[] = str_replace(
1448 _T("Status #%id does not exists in database.")
1452 } catch (Throwable
$e) {
1454 'An error occurred checking status existence: ' . $e->getMessage(),
1457 $this->errors
[] = _T("An error has occurred while looking if status does exists.");
1461 if (in_array($value, [self
::NC
, self
::MAN
, self
::WOMAN
])) {
1462 $this->$prop = (int)$value;
1464 $this->errors
[] = _T("Gender %gender does not exists!");
1468 $this->$prop = ($value instanceof Adherent
) ?
(int)$value->id
: (int)$value;
1469 $this->loadParent();
1479 public function store(): bool
1481 global $hist, $emitter, $login;
1484 if (!$login->isAdmin() && !$login->isStaff() && !$login->isGroupManager() && $this->id
== '') {
1485 if ($this->preferences
->pref_bool_create_member
) {
1486 $this->_parent
= $login->id
;
1492 $fields = self
::getDbFields($this->zdb
);
1494 foreach ($fields as $field) {
1496 $field !== 'date_modif_adh'
1497 ||
empty($this->_id
)
1499 $prop = '_' . $this->fields
[$field]['propname'];
1501 ($field === 'bool_admin_adh'
1502 ||
$field === 'bool_exempt_adh'
1503 ||
$field === 'bool_display_info'
1504 ||
$field === 'activite_adh')
1505 && $this->$prop === false
1507 //Handle booleans for postgres ; bugs #18899 and #19354
1508 $values[$field] = $this->zdb
->isPostgres() ?
'false' : 0;
1509 } elseif ($field === 'parent_id') {
1511 if ($this->_parent
=== null) {
1512 $values['parent_id'] = new Expression('NULL');
1513 } elseif ($this->parent
instanceof Adherent
) {
1514 $values['parent_id'] = $this->_parent
->id
;
1516 $values['parent_id'] = $this->_parent
;
1519 $values[$field] = $this->$prop;
1524 //an empty value will cause date to be set to 1901-01-01, a null
1525 //will result in 0000-00-00. We want a database NULL value here.
1526 if (!$this->_birthdate
) {
1527 $values['ddn_adh'] = new Expression('NULL');
1529 if (!$this->_due_date
) {
1530 $values['date_echeance'] = new Expression('NULL');
1533 if ($this->_title
instanceof Title
) {
1534 $values['titre_adh'] = $this->_title
->id
;
1536 $values['titre_adh'] = new Expression('NULL');
1539 if (!$this->_parent
) {
1540 $values['parent_id'] = new Expression('NULL');
1543 if (!$this->_number
) {
1544 $values['num_adh'] = new Expression('NULL');
1547 //fields that cannot be null
1549 '_surname' => 'prenom_adh',
1550 '_nickname' => 'pseudo_adh',
1551 '_address' => 'adresse_adh',
1552 '_zipcode' => 'cp_adh',
1553 '_town' => 'ville_adh'
1555 foreach ($notnull as $prop => $field) {
1556 if ($this->$prop === null) {
1557 $values[$field] = '';
1562 if (empty($this->_id
)) {
1563 //we're inserting a new member
1564 unset($values[self
::PK
]);
1565 //set modification date
1566 $this->_modification_date
= date('Y-m-d');
1567 $values['date_modif_adh'] = $this->_modification_date
;
1569 $insert = $this->zdb
->insert(self
::TABLE
);
1570 $insert->values($values);
1571 $add = $this->zdb
->execute($insert);
1572 if ($add->count() > 0) {
1573 $this->_id
= $this->zdb
->getLastGeneratedValue($this);
1574 $this->_picture
= new Picture($this->_id
);
1576 if ($this->_self_adh
) {
1578 _T("Self_subscription as a member: ") .
1579 $this->getNameWithCase($this->_name
, $this->_surname
),
1584 _T("Member card added"),
1590 $event = 'member.add';
1592 $hist->add(_T("Fail to add new member."));
1593 throw new \
Exception(
1594 'An error occurred inserting new member!'
1598 //we're editing an existing member
1599 if (!$this->isDueFree()) {
1601 $due_date = Contribution
::getDueDate($this->zdb
, $this->_id
);
1603 $values['date_echeance'] = $due_date;
1607 if (!$this->_password
) {
1608 unset($values['mdp_adh']);
1611 $update = $this->zdb
->update(self
::TABLE
);
1612 $update->set($values);
1613 $update->where([self
::PK
=> $this->_id
]);
1615 $edit = $this->zdb
->execute($update);
1617 //edit == 0 does not mean there were an error, but that there
1618 //were nothing to change
1619 if ($edit->count() > 0) {
1620 $this->updateModificationDate();
1622 _T("Member card updated"),
1627 $event = 'member.edit';
1632 $success = $this->dynamicsStore();
1633 $this->storeSocials($this->id
);
1636 //send event at the end of process, once all has been stored
1637 if ($event !== null) {
1638 $emitter->dispatch(new GaletteEvent($event, $this));
1641 } catch (Throwable
$e) {
1643 'Something went wrong :\'( | ' . $e->getMessage() . "\n" .
1644 $e->getTraceAsString(),
1652 * Update member modification date
1656 private function updateModificationDate(): void
1659 $modif_date = date('Y-m-d');
1660 $update = $this->zdb
->update(self
::TABLE
);
1662 array('date_modif_adh' => $modif_date)
1663 )->where([self
::PK
=> $this->_id
]);
1665 $edit = $this->zdb
->execute($update);
1666 $this->_modification_date
= $modif_date;
1667 } catch (Throwable
$e) {
1669 'Something went wrong updating modif date :\'( | ' .
1670 $e->getMessage() . "\n" . $e->getTraceAsString(),
1678 * Global getter method
1680 * @param string $name name of the property we want to retrieve
1684 public function __get(string $name)
1687 'admin', 'staff', 'due_free', 'appears_in_list', 'active',
1688 'row_classes', 'oldness', 'duplicate', 'groups', 'managed_groups'
1690 if (!defined('GALETTE_TESTS')) {
1691 $forbidden[] = 'password'; //keep that for tests only
1695 'sadmin', 'sstaff', 'sdue_free', 'sappears_in_list', 'sactive',
1696 'stitle', 'sstatus', 'sfullname', 'sname', 'saddress',
1697 'rbirthdate', 'sgender', 'contribstatus',
1700 $socials = array('website', 'msn', 'jabber', 'icq');
1702 if (in_array($name, $forbidden)) {
1704 'Calling property "' . $name . '" directly is discouraged.',
1709 return $this->isAdmin();
1711 return $this->isStaff();
1713 return $this->isDueFree();
1714 case 'appears_in_list':
1715 return $this->appearsInMembersList();
1717 return $this->isActive();
1719 return $this->isDuplicate();
1721 return $this->getGroups();
1722 case 'managed_groups':
1723 return $this->getManagedGroups();
1725 throw new \
RuntimeException("Call to __get for '$name' is forbidden!");
1729 if (in_array($name, $virtuals)) {
1730 if (substr($name, 0, 1) !== '_') {
1731 $real = '_' . substr($name, 1);
1737 return (($this->isAdmin()) ?
_T("Yes") : _T("No"));
1739 return (($this->isDueFree()) ?
_T("Yes") : _T("No"));
1740 case 'sappears_in_list':
1741 return (($this->appearsInMembersList()) ?
_T("Yes") : _T("No"));
1743 return (($this->$real) ?
_T("Yes") : _T("No"));
1745 return (($this->isActive()) ?
_T("Active") : _T("Inactive"));
1747 if (isset($this->_title
) && $this->_title
instanceof Title
) {
1748 return $this->_title
->tshort
;
1753 $status = new Status($this->zdb
);
1754 return $status->getLabel($this->_status
);
1756 return $this->getNameWithCase(
1759 (isset($this->_title
) ?
$this->title
: false)
1762 $address = $this->_address
;
1765 return $this->getNameWithCase($this->_name
, $this->_surname
);
1767 return $this->_birthdate
;
1769 switch ($this->gender
) {
1775 return _T('Unspecified');
1777 case 'contribstatus':
1778 return $this->getDues();
1782 //for backward compatibility
1783 if (in_array($name, $socials)) {
1784 $values = Social
::getListForMember($this->_id
, $name);
1785 return $values[0] ??
null;
1788 if (substr($name, 0, 1) !== '_') {
1789 $rname = '_' . $name;
1797 if ($this->$rname !== null) {
1798 return (int)$this->$rname;
1803 return $this->$rname ??
'';
1805 case 'creation_date':
1806 case 'modification_date':
1808 if ($this->$rname != '') {
1810 $d = new \
DateTime($this->$rname);
1811 return $d->format(__("Y-m-d"));
1812 } catch (Throwable
$e) {
1813 //oops, we've got a bad date :/
1815 'Bad date (' . $this->$rname . ') | ' .
1819 return $this->$rname;
1824 return ($this->_parent
instanceof Adherent
) ?
(int)$this->_parent
->id
: (int)$this->_parent
;
1826 if (!property_exists($this, $rname)) {
1828 "Unknown property '$rname'",
1833 return $this->$rname;
1839 * Global isset method
1840 * Required for twig to access properties via __get
1842 * @param string $name name of the property we want to retrieve
1846 public function __isset(string $name)
1849 'admin', 'staff', 'due_free', 'appears_in_list', 'active',
1850 'row_classes', 'oldness', 'duplicate', 'groups', 'managed_groups'
1852 if (!defined('GALETTE_TESTS')) {
1853 $forbidden[] = 'password'; //keep that for tests only
1857 'sadmin', 'sstaff', 'sdue_free', 'sappears_in_list', 'sactive',
1858 'stitle', 'sstatus', 'sfullname', 'sname', 'saddress',
1859 'rbirthdate', 'sgender', 'contribstatus',
1862 $socials = array('website', 'msn', 'jabber', 'icq');
1864 if (in_array($name, $forbidden)) {
1866 'Calling property "' . $name . '" directly is discouraged.',
1873 case 'appears_in_list':
1877 case 'managed_groups':
1884 if (in_array($name, $virtuals)) {
1888 //for backward compatibility
1889 if (in_array($name, $socials)) {
1893 if (substr($name, 0, 1) !== '_') {
1894 $rname = '_' . $name;
1904 case 'creation_date':
1905 case 'modification_date':
1910 return property_exists($this, $rname);
1918 * If member does not have an email address, but is attached to
1919 * another member, we'll take information from its parent.
1923 public function getEmail(): string
1925 $email = $this->_email
;
1926 if (empty($email)) {
1927 $this->loadParent();
1928 $email = $this->parent
->email
;
1931 //@phpstan-ignore-next-line
1932 return $email ??
'';
1936 * Get member address.
1937 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1941 public function getAddress(): string
1943 $address = $this->_address
;
1944 if (empty($address) && $this->hasParent()) {
1945 $this->loadParent();
1946 $address = $this->parent
->address
;
1949 return $address ??
'';
1953 * Get member zipcode.
1954 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1958 public function getZipcode(): string
1960 $address = $this->_address
;
1961 $zip = $this->_zipcode
;
1962 if (empty($address) && $this->hasParent()) {
1963 $this->loadParent();
1964 $zip = $this->parent
->zipcode
;
1972 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1976 public function getTown(): string
1978 $address = $this->_address
;
1979 $town = $this->_town
;
1980 if (empty($address) && $this->hasParent()) {
1981 $this->loadParent();
1982 $town = $this->parent
->town
;
1989 * Get member country.
1990 * If member does not have an address, but is attached to another member, we'll take information from its parent.
1994 public function getCountry(): string
1996 $address = $this->_address
;
1997 $country = $this->_country
;
1998 if (empty($address) && $this->hasParent()) {
1999 $this->loadParent();
2000 $country = $this->parent
->country
;
2003 return $country ??
'';
2011 public function getAge(): string
2013 if ($this->_birthdate
== null) {
2017 $d = \DateTime
::createFromFormat('Y-m-d', $this->_birthdate
);
2020 'Invalid birthdate: ' . $this->_birthdate
,
2028 $d->diff(new \
DateTime())->y
,
2029 _T(' (%age years old)')
2034 * Get parent inherited fields
2038 public function getParentFields(): array
2040 return $this->parent_fields
;
2044 * Handle files (photo and dynamics files)
2046 * @param array $files Files sent
2048 * @return array|true
2050 public function handleFiles(array $files)
2054 if (isset($files['photo'])) {
2055 if ($files['photo']['error'] === UPLOAD_ERR_OK
) {
2056 if ($files['photo']['tmp_name'] != '') {
2057 if (is_uploaded_file($files['photo']['tmp_name'])) {
2058 $res = $this->picture
->store($files['photo']);
2061 = $this->picture
->getErrorMessage($res);
2065 } elseif ($files['photo']['error'] !== UPLOAD_ERR_NO_FILE
) {
2067 $this->picture
->getPhpErrorMessage($files['photo']['error']),
2070 $this->errors
[] = $this->picture
->getPhpErrorMessage(
2071 $files['photo']['error']
2075 $this->dynamicsFiles($files);
2077 if (count($this->errors
) > 0) {
2079 'Some errors has been thew attempting to edit/store a member files' . "\n" .
2080 print_r($this->errors
, true),
2083 return $this->errors
;
2090 * Set member as duplicate
2094 public function setDuplicate(): void
2096 //mark as duplicated
2097 $this->_duplicate
= true;
2098 $infos = $this->_others_infos_admin
;
2099 $this->_others_infos_admin
= str_replace(
2101 [$this->sname
, $this->_id
],
2102 _T('Duplicated from %name (%id)')
2104 if (!empty($infos)) {
2105 $this->_others_infos_admin
.= "\n" . $infos;
2109 //drop email, must be unique
2110 $this->_email
= null;
2111 //drop creation date
2112 $this->_creation_date
= date("Y-m-d");
2114 $this->_login
= null;
2116 $this->_picture
= new Picture();
2118 $this->_birthdate
= null;
2120 $this->_surname
= null;
2122 $this->_admin
= false;
2124 $this->_due_free
= false;
2128 * Get current errors
2132 public function getErrors(): array
2134 return $this->errors
;
2142 public function getGroups(): array
2144 if (!$this->isDepEnabled('groups')) {
2145 $this->loadGroups();
2147 return $this->_groups
;
2151 * Get user managed groups
2155 public function getManagedGroups(): array
2157 if (!$this->isDepEnabled('groups')) {
2158 $this->loadGroups();
2160 return $this->_managed_groups
;
2164 * Can current logged-in user create member
2166 * @param Login $login Login instance
2170 public function canCreate(Login
$login): bool
2172 global $preferences;
2174 if ($this->id
&& $login->id
== $this->id ||
$login->isAdmin() ||
$login->isStaff()) {
2178 if ($preferences->pref_bool_groupsmanagers_create_member
&& $login->isGroupManager()) {
2182 if ($preferences->pref_bool_create_member
&& $login->isLogged()) {
2190 * Can current logged-in user edit member
2192 * @param Login $login Login instance
2196 public function canEdit(Login
$login): bool
2198 global $preferences;
2200 //admin and staff users can edit, as well as member itself
2201 if ($this->id
&& $login->id
== $this->id ||
$login->isAdmin() ||
$login->isStaff()) {
2205 //parent can edit their child cards
2206 if ($this->hasParent() && $this->parent_id
=== $login->id
) {
2210 //group managers can edit members of groups they manage when pref is on
2211 if ($preferences->pref_bool_groupsmanagers_edit_member
&& $login->isGroupManager()) {
2212 foreach ($this->getGroups() as $g) {
2213 if ($login->isGroupManager($g->getId())) {
2223 * Can current logged-in user display member
2225 * @param Login $login Login instance
2229 public function canShow(Login
$login): bool
2231 //group managers can show members of groups they manage
2232 if ($login->isGroupManager()) {
2233 foreach ($this->getGroups() as $g) {
2234 if ($login->isGroupManager($g->getId())) {
2240 return $this->canEdit($login);
2244 * Are we currently duplicated a member?
2248 public function isDuplicate(): bool
2250 return $this->_duplicate
;
2254 * Flag creation mail sending
2256 * @param boolean $send True (default) to send creation email
2260 public function setSendmail(bool $send = true): self
2262 $this->sendmail
= $send;
2267 * Should we send administrative emails to member?
2271 public function sendEMail(): bool
2273 return $this->sendmail
;
2279 * @param integer $id Parent identifier
2283 public function setParent(int $id): self
2285 $this->_parent
= $id;
2286 $this->loadParent();
2291 * Reset dependencies to load
2295 public function disableAllDeps(): self
2297 foreach ($this->_deps
as &$dep) {
2304 * Enable all dependencies to load
2308 public function enableAllDeps(): self
2310 foreach ($this->_deps
as &$dep) {
2317 * Enable a load dependency
2319 * @param string $name Dependency name
2323 public function enableDep(string $name): self
2325 if (!isset($this->_deps
[$name])) {
2327 'dependency ' . $name . ' does not exists!',
2331 $this->_deps
[$name] = true;
2338 * Enable a load dependency
2340 * @param string $name Dependency name
2344 public function disableDep(string $name): self
2346 if (!isset($this->_deps
[$name])) {
2348 'dependency ' . $name . ' does not exists!',
2352 $this->_deps
[$name] = false;
2359 * Is load dependency enabled?
2361 * @param string $name Dependency name
2365 protected function isDepEnabled(string $name): bool
2367 return $this->_deps
[$name];