Tilmeld Server API  1.0.0
Nymph user and group management with access controls.
User.php
1 <?php namespace Tilmeld\Entities;
2 
4 use Nymph\Nymph;
5 
59 class User extends AbleObject {
60  const ETYPE = 'tilmeld_user';
61  const DEFAULT_CLIENT_ENABLED_METHODS = [
62  'checkUsername',
63  'checkEmail',
64  'checkPhone',
65  'getAvatar',
66  'register',
67  ];
68  const DEFAULT_PRIVATE_DATA = [
69  'email',
70  'originalEmail',
71  'phone',
72  'addressType',
73  'addressStreet',
74  'addressStreet2',
75  'addressCity',
76  'addressState',
77  'addressZip',
78  'addressInternational',
79  'group',
80  'groups',
81  'abilities',
82  'inheritAbilities',
83  'timezone',
84  'recoverSecret',
85  'recoverSecretTime',
86  'password',
87  'passwordTemp',
88  'salt',
89  'secret',
90  'cancelEmailAddress',
91  'cancelEmailSecret',
92  'emailChangeDate',
93  ];
94  const DEFAULT_WHITELIST_DATA = [];
95  protected $tags = [];
96  protected $clientEnabledMethods = User::DEFAULT_CLIENT_ENABLED_METHODS;
97  public static $clientEnabledStaticMethods = [
98  'current',
99  'loginUser',
100  'sendRecoveryLink',
101  'recover',
102  'getClientConfig',
103  ];
104  protected $privateData = User::DEFAULT_PRIVATE_DATA;
105  public static $searchRestrictedData = User::DEFAULT_PRIVATE_DATA;
106  protected $whitelistData = User::DEFAULT_WHITELIST_DATA;
107  protected $whitelistTags = [];
108 
118  private $gatekeeperCache = [];
119 
126  private $skipAcWhenSaving = false;
127 
134  private $descendantGroups = null;
135 
142  public function __construct($id = 0) {
143  if ((is_int($id) && $id > 0) || is_string($id)) {
144  if (is_int($id)) {
145  $entity = Nymph::getEntity(
146  ['class' => get_class($this)],
147  ['&', 'guid' => $id]
148  );
149  } else {
150  $entity = Nymph::getEntity(
151  ['class' => get_class($this)],
152  ['&',
153  'ilike' => [
154  'username',
155  str_replace(['\\', '%', '_'], ['\\\\\\\\', '\%', '\_'], $id)
156  ]
157  ]
158  );
159  }
160  if (isset($entity)) {
161  $this->guid = $entity->guid;
162  $this->tags = $entity->tags;
163  $this->cdate = $entity->cdate;
164  $this->mdate = $entity->mdate;
165  $this->putData($entity->getData(), $entity->getSData());
166  if (!isset($this->secret)
167  && (
168  !isset($this->emailChangeDate)
169  || $this->emailChangeDate <
170  strtotime('-'.Tilmeld::$config['email_rate_limit'])
171  )
172  ) {
173  $this->originalEmail = $this->email;
174  }
175  return $this;
176  }
177  }
178  // Defaults.
179  $this->enabled = true;
180  $this->abilities = [];
181  $this->groups = [];
182  $this->inheritAbilities = true;
183  $this->addressType = 'us';
184  $this->updateDataProtection();
185  }
186 
193  public function &__get($name) {
194  if (Tilmeld::$config['email_usernames'] && $name == 'username') {
195  if (parent::__get('email')) {
196  return parent::__get('email');
197  }
198  return parent::__get('username');
199  }
200  return parent::__get($name);
201  }
202 
209  public function __isset($name) {
210  if (Tilmeld::$config['email_usernames'] && $name == 'username') {
211  return (parent::__isset('email') || parent::__isset('username'));
212  }
213  return parent::__isset($name);
214  }
215 
223  public function __set($name, $value) {
224  if (Tilmeld::$config['email_usernames']
225  && ($name == 'username'
226  || $name == 'email')
227  ) {
228  parent::__set('username', $value);
229  return parent::__set('email', $value);
230  }
231  return parent::__set($name, $value);
232  }
233 
239  public function __unset($name) {
240  if (Tilmeld::$config['email_usernames']
241  && ($name == 'username'
242  || $name == 'email')
243  ) {
244  parent::__unset('username');
245  return parent::__unset('email');
246  }
247  return parent::__unset($name);
248  }
249 
250  public static function current($returnObjectIfNotExist = false) {
251  if (!isset(Tilmeld::$currentUser)) {
252  return $returnObjectIfNotExist ? self::factory() : null;
253  }
254  return Tilmeld::$currentUser;
255  }
256 
263  public static function sendRecoveryLink($data) {
264  if (!Tilmeld::$config['pw_recovery']) {
265  return [
266  'result' => false,
267  'message' => 'Account recovery is not allowed.'
268  ];
269  }
270 
271  if (!Tilmeld::$config['email_usernames']
272  && $data['recoveryType'] === 'username'
273  ) {
274  // Create a username recovery email.
275 
276  $user = Nymph::getEntity(
277  ['class' => '\Tilmeld\Entities\User', 'skip_ac' => true],
278  ['&',
279  'ilike' => [
280  'email',
281  str_replace(
282  ['\\', '%', '_'],
283  ['\\\\\\\\', '\%', '\_'],
284  $data['account']
285  )
286  ]
287  ]
288  );
289 
290  if (!isset($user)) {
291  return [
292  'result' => false,
293  'message' => 'Requested account is not accessible.'
294  ];
295  }
296 
297  // Send the recovery email.
298  $macros = [
299  'to_phone' => htmlspecialchars(
300  \uMailPHP\Mail::formatPhone($user->phone)
301  ),
302  'to_timezone' => htmlspecialchars($user->timezone),
303  'to_address' =>
304  $user->addressType == 'us'
305  ?
306  htmlspecialchars(
307  "{$user->addressStreet} {$user->addressStreet2}"
308  ).'<br />'.
309  htmlspecialchars(
310  "{$user->addressCity}, {$user->addressState} ".
311  "{$user->addressZip}"
312  )
313  : '<pre>'.htmlspecialchars($user->addressInternational).'</pre>'
314  ];
315  $mail = new \uMailPHP\Mail(
316  '\Tilmeld\Entities\Mail\RecoverUsername',
317  $user,
318  $macros
319  );
320  } elseif ($data['recoveryType'] === 'password') {
321  // Create a password recovery email.
322 
323  $user = User::factory($data['account']);
324 
325  if (!isset($user->guid)) {
326  return [
327  'result' => false,
328  'message' => 'Requested account is not accessible.'
329  ];
330  }
331 
332  // Create a unique secret.
333  $user->recoverSecret = self::generateSecret($user);
334  $user->recoverSecretTime = time();
335  if (!$user->saveSkipAC()) {
336  return ['result' => false, 'message' => 'Couldn\'t save user secret.'];
337  }
338 
339  // Send the recovery email.
340  $macros = [
341  'recover_code' => $user->recoverSecret,
342  'time_limit' => htmlspecialchars(
343  Tilmeld::$config['pw_recovery_time_limit']
344  ),
345  'to_phone' => htmlspecialchars(
346  \uMailPHP\Mail::formatPhone($user->phone)
347  ),
348  'to_timezone' => htmlspecialchars($user->timezone),
349  'to_address' =>
350  $user->addressType == 'us'
351  ?
352  htmlspecialchars(
353  "{$user->addressStreet} {$user->addressStreet2}"
354  ).'<br />'.
355  htmlspecialchars(
356  "{$user->addressCity}, {$user->addressState} ".
357  "{$user->addressZip}"
358  )
359  : '<pre>'.htmlspecialchars($user->addressInternational).'</pre>'
360  ];
361  $mail = new \uMailPHP\Mail(
362  '\Tilmeld\Entities\Mail\RecoverPassword',
363  $user,
364  $macros
365  );
366  } else {
367  return ['result' => false, 'message' => 'Invalid recovery type.'];
368  }
369 
370  // Send the email.
371  if ($mail->send()) {
372  return [
373  'result' => true,
374  'message' => 'We\'ve sent an email to your registered address. Please '.
375  'check your email to continue with account recovery.'
376  ];
377  } else {
378  return ['result' => false, 'message' => 'Couldn\'t send recovery email.'];
379  }
380  }
381 
388  public static function recover($data) {
389  if (!Tilmeld::$config['pw_recovery']) {
390  return [
391  'result' => false,
392  'message' => 'Account recovery is not allowed.'
393  ];
394  }
395 
396  $user = User::factory($data['username']);
397 
398  if (!isset($user->guid)
399  || !isset($user->recoverSecret)
400  || $data['secret'] !== $user->recoverSecret
401  || strtotime(
402  '+'.Tilmeld::$config['pw_recovery_time_limit'],
403  $user->recoverSecretTime
404  ) < time()
405  ) {
406  return [
407  'result' => false,
408  'message' => 'The secret code does not match.'
409  ];
410  }
411 
412  if (empty($data['password'])) {
413  return ['result' => false, 'message' => 'Password cannot be empty.'];
414  }
415 
416  $user->password($data['password']);
417  unset($user->recoverSecret);
418  unset($user->recoverSecretTime);
419  if ($user->saveSkipAC()) {
420  return [
421  'result' => true,
422  'message' => 'Your password has been reset. You can now log in using '.
423  'your new password.'
424  ];
425  } else {
426  return ['result' => false, 'message' => 'Error saving new password.'];
427  }
428  }
429 
430  public static function getClientConfig() {
431  $timezones = \DateTimeZone::listIdentifiers();
432  sort($timezones);
433  return (object) [
434  'reg_fields' => Tilmeld::$config['reg_fields'],
435  'email_usernames' => Tilmeld::$config['email_usernames'],
436  'allow_registration' => Tilmeld::$config['allow_registration'],
437  'pw_recovery' => Tilmeld::$config['pw_recovery'],
438  'verify_email' => Tilmeld::$config['verify_email'],
439  'unverified_access' => Tilmeld::$config['unverified_access'],
440  'timezones' => $timezones,
441  ];
442  }
443 
444  public static function generateSecret($user) {
445  return substr(
446  hash('sha256', uniqid($user->username, true)),
447  0,
448  rand(12, 18)
449  );
450  }
451 
452  public static function loginUser($data) {
453  if (!isset($data['username'])) {
454  return ['result' => false, 'message' => 'Incorrect login/password.'];
455  }
456  $user = User::factory($data['username']);
457  $result = $user->login($data);
458  if ($result['result']) {
459  $user->updateDataProtection();
460  $result['user'] = $user;
461  }
462  return $result;
463  }
464 
465  public function login($data) {
466  if (!isset($this->guid)) {
467  return ['result' => false, 'message' => 'Incorrect login/password.'];
468  }
469  if (!$this->enabled) {
470  return ['result' => false, 'message' => 'This user is disabled.'];
471  }
472  if ($this->gatekeeper()) {
473  return ['result' => true, 'message' => 'You are already logged in.'];
474  }
475  if (!$this->checkPassword($data['password'])) {
476  return ['result' => false, 'message' => 'Incorrect login/password.'];
477  }
478 
479  // Authentication was successful, attempt to login.
480  if (!Tilmeld::login($this, true)) {
481  return ['result' => false, 'message' => 'Incorrect login/password.'];
482  }
483 
484  // Login was successful.
485  return ['result' => true, 'message' => 'You are logged in.'];
486  }
487 
493  public function logout() {
494  Tilmeld::logout();
495  return ['result' => true, 'message' => 'You have been logged out.'];
496  }
497 
498  public function getAvatar() {
499  if (isset($this->avatar)) {
500  return $this->avatar;
501  }
502  $proto = isset($_SERVER['HTTPS']) ? 'https' : 'http';
503  if (!isset($this->email) || empty($this->email)) {
504  return $proto.'://secure.gravatar.com/avatar/?d=mm&s=40';
505  }
506  return $proto.'://secure.gravatar.com/avatar/'.
507  md5(strtolower(trim($this->email))).'?d=identicon&s=40';
508  }
509 
513  public function getDescendantGroups() {
514  if (!isset($this->descendantGroups)) {
515  $this->descendantGroups = [];
516  if (isset($this->group)) {
517  $this->descendantGroups =
518  (array) $this->group->getDescendants();
519  }
520  foreach ($this->groups as $curGroup) {
521  $this->descendantGroups =
522  array_merge(
523  (array) $this->descendantGroups,
524  (array) $curGroup->getDescendants()
525  );
526  }
527  }
528  return $this->descendantGroups;
529  }
530 
544  public function getTimezone($returnDateTimeZoneObject = false) {
545  $timezone = date_default_timezone_get();
546  if (!empty($this->timezone)) {
547  $timezone = $this->timezone;
548  } elseif (isset($this->group->guid) && !empty($this->group->timezone)) {
549  $timezone = $this->group->timezone;
550  } else if (count($this->groups)) {
551  foreach ((array) $this->groups as $curGroup) {
552  if (!empty($curGroup->timezone)) {
553  $timezone = $curGroup->timezone;
554  break;
555  }
556  }
557  }
558  return $returnDateTimeZoneObject
559  ? new DateTimeZone($timezone)
560  : $timezone;
561  }
562 
563  public function putData($data, $sdata = []) {
564  $return = parent::putData($data, $sdata);
565  $this->updateDataProtection();
566  return $return;
567  }
568 
576  public function updateDataProtection($user = null) {
577  if (!isset($user)) {
578  $user = self::current();
579  }
580 
581  $this->clientEnabledMethods = self::DEFAULT_CLIENT_ENABLED_METHODS;
582  $this->privateData = self::DEFAULT_PRIVATE_DATA;
583  $this->whitelistData = self::DEFAULT_WHITELIST_DATA;
584 
585  if (Tilmeld::$config['email_usernames']) {
586  $this->privateData[] = 'username';
587  }
588 
589  $isCurrentUser = $user !== null && $this->is($user);
590  $isNewUser = !isset($this->guid);
591 
592  if ($isCurrentUser) {
593  // Users can check to see what abilities they have.
594  $this->clientEnabledMethods[] = 'gatekeeper';
595  $this->clientEnabledMethods[] = 'changePassword';
596  $this->clientEnabledMethods[] = 'logout';
597  $this->clientEnabledMethods[] = 'sendEmailVerification';
598  }
599 
600  if ($user !== null && $user->gatekeeper('tilmeld/admin')) {
601  // Users who can edit other users can see most of their data.
602  $this->privateData = [
603  'password',
604  'salt'
605  ];
606  $this->whitelistData = false;
607  } elseif ($isCurrentUser || $isNewUser) {
608  // Users can see their own data, and edit some of it.
609  $this->whitelistData[] = 'username';
610  $this->whitelistData[] = 'avatar';
611  if (in_array('name', Tilmeld::$config['user_fields'])) {
612  $this->whitelistData[] = 'nameFirst';
613  $this->whitelistData[] = 'nameMiddle';
614  $this->whitelistData[] = 'nameLast';
615  $this->whitelistData[] = 'name';
616  }
617  if (in_array('email', Tilmeld::$config['user_fields'])) {
618  $this->whitelistData[] = 'email';
619  }
620  if (in_array('phone', Tilmeld::$config['user_fields'])) {
621  $this->whitelistData[] = 'phone';
622  }
623  if (in_array('timezone', Tilmeld::$config['user_fields'])) {
624  $this->whitelistData[] = 'timezone';
625  }
626  if (in_array('address', Tilmeld::$config['user_fields'])) {
627  $this->whitelistData[] = 'addressType';
628  $this->whitelistData[] = 'addressStreet';
629  $this->whitelistData[] = 'addressStreet2';
630  $this->whitelistData[] = 'addressCity';
631  $this->whitelistData[] = 'addressState';
632  $this->whitelistData[] = 'addressZip';
633  $this->whitelistData[] = 'addressInternational';
634  }
635  $this->privateData = [
636  'originalEmail',
637  'secret',
638  'cancelEmailAddress',
639  'cancelEmailSecret',
640  'emailChangeDate',
641  'recoverSecret',
642  'recoverSecretTime',
643  'password',
644  'salt'
645  ];
646  }
647  }
648 
663  public function gatekeeper($ability = null) {
664  if (!isset($ability)) {
665  return self::current(true)->is($this);
666  }
667  // Check the cache to see if we've already checked this user.
668  if ($this->gatekeeperCache) {
669  $abilities =& $this->gatekeeperCache;
670  } else {
671  $abilities = $this->abilities;
672  if ($this->inheritAbilities) {
673  foreach ($this->groups as &$curGroup) {
674  if (!isset($curGroup->guid)) {
675  continue;
676  }
677  $abilities = array_merge($abilities, $curGroup->abilities);
678  }
679  unset($curGroup);
680  if (isset($this->group) && isset($this->group->guid)) {
681  $abilities = array_merge($abilities, $this->group->abilities);
682  }
683  }
684  $this->gatekeeperCache = $abilities;
685  }
686  if (!is_array($abilities)) {
687  return false;
688  }
689  return (
690  in_array($ability, $abilities) || in_array('system/admin', $abilities)
691  );
692  }
693 
694  public function clearCache() {
695  $return = parent::clearCache();
696  $this->gatekeeperCache = [];
697  return $return;
698  }
699 
705  public function sendEmailVerification() {
706  if (!isset($this->guid)) {
707  return false;
708  }
709  $success = true;
710  if (isset($this->secret) && !isset($this->cancelEmailSecret)) {
711  // phpcs:ignore Generic.Files.LineLength.TooLong
712  $link = htmlspecialchars(Tilmeld::$config['setup_url'].(strpos(Tilmeld::$config['setup_url'], '?') ? '&' : '?').'action=verifyemail&id='.$this->guid.'&secret='.$this->secret);
713  $macros = [
714  'verify_link' => $link,
715  'to_phone' => htmlspecialchars(
716  \uMailPHP\Mail::formatPhone($this->phone)
717  ),
718  'to_timezone' => htmlspecialchars($this->timezone),
719  'to_address' =>
720  $this->addressType == 'us'
721  ?
722  htmlspecialchars(
723  "{$this->addressStreet} {$this->addressStreet2}"
724  ).'<br />'.
725  htmlspecialchars(
726  "{$this->addressCity}, {$this->addressState} ".
727  "{$this->addressZip}"
728  )
729  : '<pre>'.htmlspecialchars($this->addressInternational).'</pre>'
730  ];
731  $mail = new \uMailPHP\Mail(
732  '\Tilmeld\Entities\Mail\VerifyEmail',
733  $this,
734  $macros
735  );
736  $success = $success && $mail->send();
737  }
738  if (isset($this->secret) && isset($this->cancelEmailSecret)) {
739  // phpcs:ignore Generic.Files.LineLength.TooLong
740  $link = htmlspecialchars(Tilmeld::$config['setup_url'].(strpos(Tilmeld::$config['setup_url'], '?') ? '&' : '?').'action=verifyemailchange&id='.$this->guid.'&secret='.$this->secret);
741  $macros = [
742  'verify_link' => $link,
743  'old_email' => htmlspecialchars($this->cancelEmailAddress),
744  'new_email' => htmlspecialchars($this->email),
745  'to_phone' => htmlspecialchars(
746  \uMailPHP\Mail::formatPhone($this->phone)
747  ),
748  'to_timezone' => htmlspecialchars($this->timezone),
749  'to_address' =>
750  $this->addressType == 'us'
751  ?
752  htmlspecialchars(
753  "{$this->addressStreet} {$this->addressStreet2}"
754  ).'<br />'.
755  htmlspecialchars(
756  "{$this->addressCity}, {$this->addressState} ".
757  "{$this->addressZip}"
758  )
759  : '<pre>'.htmlspecialchars($this->addressInternational).'</pre>'
760  ];
761  $mail = new \uMailPHP\Mail(
762  '\Tilmeld\Entities\Mail\VerifyEmailChange',
763  $this,
764  $macros
765  );
766  $success = $success && $mail->send();
767  }
768  if (isset($this->cancelEmailSecret)) {
769  // phpcs:ignore Generic.Files.LineLength.TooLong
770  $link = htmlspecialchars(Tilmeld::$config['setup_url'].(strpos(Tilmeld::$config['setup_url'], '?') ? '&' : '?').'action=cancelemailchange&id='.$this->guid.'&secret='.$this->cancelEmailSecret);
771  $macros = [
772  'cancel_link' => $link,
773  'old_email' => htmlspecialchars($this->cancelEmailAddress),
774  'new_email' => htmlspecialchars($this->email),
775  'to_phone' => htmlspecialchars(
776  \uMailPHP\Mail::formatPhone($this->phone)
777  ),
778  'to_timezone' => htmlspecialchars($this->timezone),
779  'to_address' =>
780  $this->addressType == 'us'
781  ?
782  htmlspecialchars(
783  "{$this->addressStreet} {$this->addressStreet2}"
784  ).'<br />'.
785  htmlspecialchars(
786  "{$this->addressCity}, {$this->addressState} ".
787  "{$this->addressZip}"
788  )
789  : '<pre>'.htmlspecialchars($this->addressInternational).'</pre>'
790  ];
791  $mail = new \uMailPHP\Mail(
792  '\Tilmeld\Entities\Mail\CancelEmailChange',
793  $this,
794  $macros
795  );
796  $success = $success && $mail->send();
797  }
798  return $success;
799  }
800 
808  public function addGroup($group) {
809  if (!$group->inArray((array) $this->groups)) {
810  $this->groups[] = $group;
811  return $this->groups;
812  }
813  return true;
814  }
815 
822  public function checkPassword($password) {
823  switch (Tilmeld::$config['pw_method']) {
824  case 'plain':
825  return ($this->password == $password);
826  case 'digest':
827  return ($this->password == hash('sha256', $password));
828  case 'salt':
829  default:
830  return ($this->password == hash('sha256', $password.$this->salt));
831  }
832  }
833 
841  public function delGroup($group) {
842  if ($group->inArray((array) $this->groups)) {
843  foreach ((array) $this->groups as $key => $curGroup) {
844  if ($group->is($curGroup)) {
845  unset($this->groups[$key]);
846  }
847  }
848  return $this->groups;
849  }
850  return true;
851  }
852 
859  public function inGroup($group = null) {
860  if (is_numeric($group)) {
861  $group = Group::factory((int) $group);
862  }
863  if (!isset($group->guid)) {
864  return false;
865  }
866  return ($group->inArray((array) $this->groups) || $group->is($this->group));
867  }
868 
875  public function isDescendant($group = null) {
876  if (is_numeric($group)) {
877  $group = Group::factory((int) $group);
878  }
879  if (!isset($group->guid)) {
880  return false;
881  }
882  // Check to see if the user is in a descendant group of the given group.
883  if (isset($this->group->guid) && $this->group->isDescendant($group)) {
884  return true;
885  }
886  foreach ((array) $this->groups as $curGroup) {
887  if ($curGroup->isDescendant($group)) {
888  return true;
889  }
890  }
891  return false;
892  }
893 
900  public function changePassword($data) {
901  if (!isset($data['password']) || (string) $data['password'] === '') {
902  return ['result' => false, 'message' => 'Please specify a password.'];
903  }
904  if (!$this->checkPassword($data['oldPassword'])) {
905  return ['result' => false, 'message' => 'Incorrect password.'];
906  }
907  $this->passwordTemp = (string) $data['password'];
908  if ($this->save()) {
909  return ['result' => true, 'message' => 'Your password has been changed.'];
910  } else {
911  return ['result' => false, 'message' => 'Couldn\'t save new password.'];
912  }
913  }
914 
921  public function password($password) {
922  switch (Tilmeld::$config['pw_method']) {
923  case 'plain':
924  unset($this->salt);
925  return $this->password = $password;
926  case 'digest':
927  unset($this->salt);
928  return $this->password = hash('sha256', $password);
929  case 'salt':
930  default:
931  $this->salt = hash('sha256', rand());
932  return $this->password = hash('sha256', $password.$this->salt);
933  }
934  }
935 
942  public function checkUsername() {
943  if (!Tilmeld::$config['email_usernames']) {
944  if (empty($this->username)) {
945  return ['result' => false, 'message' => 'Please specify a username.'];
946  }
947  if (Tilmeld::$config['max_username_length'] > 0
948  && strlen($this->username) > Tilmeld::$config['max_username_length']
949  ) {
950  return [
951  'result' => false,
952  'message' => 'Usernames must not exceed '.
953  Tilmeld::$config['max_username_length'].' characters.'
954  ];
955  }
956  if (array_diff(
957  str_split($this->username),
958  str_split(Tilmeld::$config['valid_chars'])
959  )) {
960  return [
961  'result' => false,
962  'message' => Tilmeld::$config['valid_chars_notice']
963  ];
964  }
965  if (!preg_match(Tilmeld::$config['valid_regex'], $this->username)) {
966  return [
967  'result' => false,
968  'message' => Tilmeld::$config['valid_regex_notice']
969  ];
970  }
971  $selector = ['&',
972  'ilike' => [
973  'username',
974  str_replace(
975  ['\\', '%', '_'],
976  ['\\\\\\\\', '\%', '\_'],
977  $this->username
978  )
979  ]
980  ];
981  if (isset($this->guid)) {
982  $selector['!guid'] = $this->guid;
983  }
984  $test = Nymph::getEntity(
985  ['class' => '\Tilmeld\Entities\User', 'skip_ac' => true],
986  $selector
987  );
988  if (isset($test->guid)) {
989  return ['result' => false, 'message' => 'That username is taken.'];
990  }
991 
992  return [
993  'result' => true,
994  'message' => (
995  isset($this->guid) ? 'Username is valid.' : 'Username is available!'
996  )
997  ];
998  } else {
999  if (empty($this->username)) {
1000  return ['result' => false, 'message' => 'Please specify an email.'];
1001  }
1002  if (Tilmeld::$config['max_username_length'] > 0
1003  && strlen($this->username) > Tilmeld::$config['max_username_length']
1004  ) {
1005  return [
1006  'result' => false,
1007  'message' => 'Emails must not exceed '.
1008  Tilmeld::$config['max_username_length'].' characters.'
1009  ];
1010  }
1011 
1012  return $this->checkEmail();
1013  }
1014  }
1015 
1022  public function checkEmail() {
1023  if (empty($this->email)) {
1024  if (Tilmeld::$config['verify_email']) {
1025  return ['result' => false, 'message' => 'Please specify an email.'];
1026  } else {
1027  return ['result' => true, 'message' => ''];
1028  }
1029  }
1030  if (!preg_match(
1031  '/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i',
1032  $this->email
1033  )) {
1034  return [
1035  'result' => false,
1036  'message' => 'Email must be a correctly formatted address.'
1037  ];
1038  }
1039  $selector = ['&',
1040  'ilike' => [
1041  'email',
1042  str_replace(['\\', '%', '_'], ['\\\\\\\\', '\%', '\_'], $this->email)
1043  ]
1044  ];
1045  if (isset($this->guid)) {
1046  $selector['!guid'] = $this->guid;
1047  }
1048  $test = Nymph::getEntity(
1049  ['class' => '\Tilmeld\Entities\User', 'skip_ac' => true],
1050  $selector
1051  );
1052  if (isset($test->guid)) {
1053  return [
1054  'result' => false,
1055  'message' => 'That email address is already registered.'
1056  ];
1057  }
1058 
1059  return [
1060  'result' => true,
1061  'message' => (
1062  isset($this->guid) ? 'Email is valid.' : 'Email address is valid!'
1063  )
1064  ];
1065  }
1066 
1073  public function checkPhone() {
1074  if (empty($this->phone)) {
1075  return ['result' => false, 'message' => 'Please specify a phone number.'];
1076  }
1077 
1078  $stripToDigits = preg_replace('/\D/', '', $this->phone);
1079  if (!preg_match('/\d{10}/', $stripToDigits)) {
1080  return [
1081  'result' => false,
1082  'message' =>
1083  'Phone must contain 10 digits, but formatting does not matter.'
1084  ];
1085  }
1086  $selector = ['&',
1087  'strict' => ['phone', $stripToDigits]
1088  ];
1089  if (isset($this->guid)) {
1090  $selector['!guid'] = $this->guid;
1091  }
1092  $test = Nymph::getEntity(
1093  ['class' => '\Tilmeld\Entities\User', 'skip_ac' => true],
1094  $selector
1095  );
1096  if (isset($test->guid)) {
1097  return ['result' => false, 'message' => 'Phone number is in use.'];
1098  }
1099 
1100  return [
1101  'result' => true,
1102  'message' => (
1103  isset($this->guid) ? 'Phone number is valid.' : 'Phone number is valid!'
1104  )
1105  ];
1106  }
1107 
1108  public function register($data) {
1109  if (!Tilmeld::$config['allow_registration']) {
1110  return [
1111  'result' => false,
1112  'loggedin' => false,
1113  'message' => 'Registration is not allowed.'
1114  ];
1115  }
1116  if (isset($this->guid)) {
1117  return [
1118  'result' => false,
1119  'loggedin' => false,
1120  'message' => 'This is already a registered user.'
1121  ];
1122  }
1123  if (!isset($data['password']) || (string) $data['password'] === '') {
1124  return [
1125  'result' => false,
1126  'loggedin' => false,
1127  'message' => 'Password is a required field.'
1128  ];
1129  }
1130  $unCheck = $this->checkUsername();
1131  if (!$unCheck['result']) {
1132  return $unCheck;
1133  }
1134 
1135  $this->password((string) $data['password']);
1136  if (in_array('name', Tilmeld::$config['reg_fields'])) {
1137  $this->name =
1138  $this->nameFirst.
1139  (!empty($this->nameMiddle) ? ' '.$this->nameMiddle : '').
1140  (!empty($this->nameLast) ? ' '.$this->nameLast : '');
1141  if ($this->name === '') {
1142  $this->name = $this->username;
1143  }
1144  }
1145  if (Tilmeld::$config['email_usernames']) {
1146  $this->email = $this->username;
1147  }
1148 
1149  // Add primary group.
1150  $primaryGroup = null;
1151  $generatedPrimaryGroup = false;
1152  if (Tilmeld::$config['generate_primary']) {
1153  // Generate a new primary group for the user.
1154  $primaryGroup = Group::factory();
1155  $primaryGroup->groupname = $this->username;
1156  $primaryGroup->avatar = $this->avatar;
1157  $primaryGroup->name = $this->name;
1158  $primaryGroup->email = $this->email;
1159  $primaryGroup->parent = Nymph::getEntity(
1160  ['class' => '\Tilmeld\Entities\Group'],
1161  ['&',
1162  'equal' => ['defaultPrimary', true]
1163  ]
1164  );
1165  if (!isset($primaryGroup->parent) || !isset($primaryGroup->group->guid)) {
1166  unset($primaryGroup->parent);
1167  }
1168  if (!$primaryGroup->saveSkipAC()) {
1169  return [
1170  'result' => false,
1171  'loggedin' => false,
1172  'message' => 'Error creating primary group for user.'
1173  ];
1174  }
1175  $this->group = $primaryGroup;
1176  $generatedPrimaryGroup = $primaryGroup;
1177  } else {
1178  // Add the default primary.
1179  $this->group = Nymph::getEntity(
1180  ['class' => '\Tilmeld\Entities\Group'],
1181  ['&',
1182  'equal' => ['defaultPrimary', true]
1183  ]
1184  );
1185  if (!isset($this->group) || !isset($this->group->guid)) {
1186  unset($this->group);
1187  }
1188  }
1189 
1190  try {
1191  // Add secondary groups.
1192  if (Tilmeld::$config['verify_email']
1193  && Tilmeld::$config['unverified_access']
1194  ) {
1195  // Add the default secondaries for unverified users.
1196  $this->groups = (array) Nymph::getEntities(
1197  ['class' => '\Tilmeld\Entities\Group'],
1198  ['&',
1199  'equal' => ['unverifiedSecondary', true]
1200  ]
1201  );
1202  } else {
1203  // Add the default secondaries.
1204  $this->groups = (array) Nymph::getEntities(
1205  ['class' => '\Tilmeld\Entities\Group'],
1206  ['&',
1207  'equal' => ['defaultSecondary', true]
1208  ]
1209  );
1210  }
1211 
1212  if (Tilmeld::$config['verify_email']) {
1213  // The user will be enabled after verifying their e-mail address.
1214  if (!Tilmeld::$config['unverified_access']) {
1215  $this->enabled = false;
1216  }
1217  } else {
1218  $this->enabled = true;
1219  }
1220 
1221  // If create_admin is true and there are no other users, grant
1222  // "system/admin".
1223  if (Tilmeld::$config['create_admin']) {
1224  $otherUsers = Nymph::getEntities(
1225  ['class' => '\Tilmeld\Entities\User', 'skip_ac' => true, 'limit' => 1]
1226  );
1227  // Make sure it's not just null, cause that means an error.
1228  if ($otherUsers === []) {
1229  $this->grant('system/admin');
1230  $this->enabled = true;
1231  }
1232  }
1233 
1234  if ($this->saveSkipAC()) {
1235  // Send the new user registered email.
1236  $macros = [
1237  'user_username' => htmlspecialchars($this->username),
1238  'user_name' => htmlspecialchars($this->name),
1239  'user_first_name' => htmlspecialchars($this->nameFirst),
1240  'user_last_name' => htmlspecialchars($this->nameLast),
1241  'user_email' => htmlspecialchars($this->email),
1242  'user_phone' => htmlspecialchars(
1243  \uMailPHP\Mail::formatPhone($this->phone)
1244  ),
1245  'user_timezone' => htmlspecialchars($this->timezone),
1246  'user_address' =>
1247  $this->addressType == 'us'
1248  ?
1249  htmlspecialchars(
1250  "{$this->addressStreet} {$this->addressStreet2}"
1251  ).'<br />'.
1252  htmlspecialchars(
1253  "{$this->addressCity}, {$this->addressState} ".
1254  "{$this->addressZip}"
1255  )
1256  : '<pre>'.htmlspecialchars($this->addressInternational).'</pre>'
1257  ];
1258  $mail = new \uMailPHP\Mail(
1259  '\Tilmeld\Entities\Mail\UserRegistered',
1260  null,
1261  $macros
1262  );
1263  $mail->send();
1264 
1265  $message = "";
1266 
1267  // Save the primary group.
1268  if ($primaryGroup) {
1269  $primaryGroup->user = $this;
1270  if (!$primaryGroup->saveSkipAC()) {
1271  $message .= "Your account was created, but your primary group ".
1272  "couldn't be assigned to you. You should ask an administrator to ".
1273  "fix this. ";
1274  }
1275  }
1276 
1277  // Finish up.
1278  if (Tilmeld::$config['verify_email']
1279  && !Tilmeld::$config['unverified_access']
1280  ) {
1281  $message .= "Almost there. An email has been sent to {$this->email} ".
1282  "with a verification link for you to finish registration.";
1283  $loggedin = false;
1284  } elseif (Tilmeld::$config['verify_email']
1285  && Tilmeld::$config['unverified_access']
1286  ) {
1287  Tilmeld::login($this, true);
1288  $this->updateDataProtection();
1289  $message .= "You're now logged in! An email has been sent to ".
1290  "{$this->email} with a verification link for you to finish ".
1291  "registration.";
1292  $loggedin = true;
1293  } else {
1294  Tilmeld::login($this, true);
1295  $this->updateDataProtection();
1296  $message .= 'You\'re now registered and logged in!';
1297  $loggedin = true;
1298  }
1299  return ['result' => true, 'loggedin' => $loggedin, 'message' => $message];
1300  } else {
1301  if ($generatedPrimaryGroup) {
1302  $generatedPrimaryGroup->delete();
1303  }
1304  return [
1305  'result' => false,
1306  'loggedin' => false,
1307  'message' => 'Error registering user.'
1308  ];
1309  }
1310  } catch (\Exception $e) {
1311  if ($generatedPrimaryGroup) {
1312  $generatedPrimaryGroup->delete();
1313  }
1314  throw $e;
1315  }
1316  }
1317 
1318  public function save() {
1319  if (!isset($this->username)) {
1320  return false;
1321  }
1322 
1323  $sendVerification = false;
1324 
1325  // Formatting.
1326  $this->username = trim($this->username);
1327  // Setting username sets both username and email if email_usernames is on.
1328  if (!Tilmeld::$config['email_usernames']) {
1329  $this->email = trim($this->email);
1330  }
1331  $this->nameFirst = trim($this->nameFirst);
1332  $this->nameMiddle = trim($this->nameMiddle);
1333  $this->nameLast = trim($this->nameLast);
1334  $this->phone = trim($this->phone);
1335  $this->name =
1336  $this->nameFirst.
1337  (!empty($this->nameMiddle) ? ' '.$this->nameMiddle : '').
1338  (!empty($this->nameLast) ? ' '.$this->nameLast : '');
1339 
1340  // Verification.
1341  $unCheck = $this->checkUsername();
1342  if (!$unCheck['result']) {
1343  throw new \Tilmeld\Exceptions\BadUsernameException($unCheck['message']);
1344  }
1345  if (!Tilmeld::$config['email_usernames']) {
1346  $emCheck = $this->checkEmail();
1347  if (!$emCheck['result']) {
1348  throw new \Tilmeld\Exceptions\BadEmailException($emCheck['message']);
1349  }
1350  }
1351 
1352  // Email changes.
1353  if (!Tilmeld::gatekeeper('tilmeld/admin')) {
1354  // The user isn't an admin, so email address changes should contain
1355  // some security measures.
1356  if (Tilmeld::$config['verify_email']) {
1357  // The user needs to verify this new email address.
1358  if (!isset($this->guid)) {
1359  $this->secret = self::generateSecret($this);
1360  $sendVerification = true;
1361  } elseif (!empty($this->originalEmail)
1362  && $this->originalEmail !== $this->email
1363  ) {
1364  // The user already has an old email address.
1365  if (Tilmeld::$config['email_rate_limit'] !== ''
1366  && isset($this->emailChangeDate)
1367  && $this->emailChangeDate >
1368  strtotime('-'.Tilmeld::$config['email_rate_limit'])
1369  ) {
1370  throw new \Tilmeld\Exceptions\EmailChangeRateLimitExceededException(
1371  'You already changed your email address recently. Please wait '.
1372  'until '.
1373  \uMailPHP\Mail::formatDate(
1374  strtotime(
1375  '+'.Tilmeld::$config['email_rate_limit'],
1376  $this->emailChangeDate
1377  ),
1378  'full_short'
1379  ).
1380  ' to change your email address again.'
1381  );
1382  } else {
1383  if (!isset($this->secret)
1384  && (
1385  // Make sure the user has at least the rate
1386  // limit time to cancel an email change.
1387  !isset($this->emailChangeDate) ||
1388  $this->emailChangeDate <
1389  strtotime('-'.Tilmeld::$config['email_rate_limit'])
1390  )
1391  ) {
1392  // Save the old email in case the cancel change
1393  // link is clicked.
1394  $this->cancelEmailAddress = $this->originalEmail;
1395  $this->cancelEmailSecret = self::generateSecret($this);
1396  $this->emailChangeDate = time();
1397  }
1398  $this->secret = self::generateSecret($this);
1399  $sendVerification = true;
1400  }
1401  }
1402  } elseif (isset($this->guid)
1403  && !empty($this->originalEmail)
1404  && $this->originalEmail !== $this->email
1405  && (
1406  // Make sure the user has at least the rate limit time
1407  // to cancel an email change.
1408  !isset($this->emailChangeDate) ||
1409  $this->emailChangeDate <
1410  strtotime('-'.Tilmeld::$config['email_rate_limit'])
1411  )
1412  ) {
1413  // The user doesn't need to verify their new email address, but
1414  // should be able to cancel the email change from their old
1415  // address.
1416  $this->cancelEmailAddress = $this->originalEmail;
1417  $this->cancelEmailSecret = self::generateSecret($this);
1418  $sendVerification = true;
1419  }
1420  }
1421 
1422  if (!isset($this->password) && !isset($this->passwordTemp)) {
1423  throw new \Tilmeld\Exceptions\BadDataException('A password is required.');
1424  }
1425 
1426  if (isset($this->passwordTemp) && $this->passwordTemp !== '') {
1427  $this->password($this->passwordTemp);
1428  }
1429  unset($this->passwordTemp);
1430 
1431  try {
1432  Tilmeld::$config['validator_user']->assert($this->getValidatable());
1433  // phpcs:ignore Generic.Files.LineLength.TooLong
1434  } catch (\Respect\Validation\Exceptions\NestedValidationException $exception) {
1435  throw new \Tilmeld\Exceptions\BadDataException(
1436  $exception->getFullMessage()
1437  );
1438  }
1439 
1440  if (isset($this->group->user) && $this->is($this->group->user)) {
1441  // Update the user's generated primary group.
1442  $this->group->groupname = $this->username;
1443  $this->group->avatar = $this->avatar;
1444  $this->group->email = $this->email;
1445  $this->group->name = $this->name;
1446  $this->group->saveSkipAC();
1447  }
1448 
1449  $return = parent::save();
1450  if ($return) {
1451  if ($sendVerification) {
1452  // The email has changed, so send a new verification email.
1453  $this->sendEmailVerification();
1454  }
1455 
1456  if (self::current(true)->is($this)) {
1457  // Update the user in the session cache.
1458  Tilmeld::fillSession($this);
1459  }
1460 
1461  $this->descendantGroups = null;
1462  }
1463  return $return;
1464  }
1465 
1469  public function saveSkipAC() {
1470  $this->skipAcWhenSaving = true;
1471  return $this->save();
1472  }
1473 
1474  public function tilmeldSaveSkipAC() {
1475  if ($this->skipAcWhenSaving) {
1476  $this->skipAcWhenSaving = false;
1477  return true;
1478  }
1479  return false;
1480  }
1481 
1482  public function delete() {
1483  if (!Tilmeld::gatekeeper('tilmeld/admin')) {
1484  return false;
1485  }
1486  if (self::current(true)->is($this)) {
1487  $this->logout();
1488  }
1489  return parent::delete();
1490  }
1491 }
getTimezone($returnDateTimeZoneObject=false)
Return the user's timezone.
Definition: User.php:544
& __get($name)
Override the magic method, for email usernames.
Definition: User.php:193
sendEmailVerification()
Send the user email verification/change/cancellation links.
Definition: User.php:705
addGroup($group)
Add the user to a (secondary) group.
Definition: User.php:808
gatekeeper($ability=null)
Check to see if a user has an ability.
Definition: User.php:663
static fillSession($user)
Fill session user data.
Definition: Tilmeld.php:383
inGroup($group=null)
Check whether the user is in a (primary or secondary) group.
Definition: User.php:859
logout()
Log a user out of the system.
Definition: User.php:493
static gatekeeper($ability=null)
Check to see if the current user has an ability.
Definition: Tilmeld.php:58
static recover($data)
Recover account details.
Definition: User.php:388
__set($name, $value)
Override the magic method, for email usernames.
Definition: User.php:223
A user entity.
Definition: User.php:59
static sendRecoveryLink($data)
Send an account recovery link.
Definition: User.php:263
grant($ability)
Grant an ability.
Definition: AbleObject.php:18
checkPassword($password)
Check the given password against the user's.
Definition: User.php:822
isDescendant($group=null)
Check whether the user is a descendant of a group.
Definition: User.php:875
changePassword($data)
A frontend accessible method to change the user's password.
Definition: User.php:900
updateDataProtection($user=null)
Update the data protection arrays for a user.
Definition: User.php:576
__unset($name)
Override the magic method, for email usernames.
Definition: User.php:239
Tilmeld main class.
Definition: Tilmeld.php:17
__isset($name)
Override the magic method, for email usernames.
Definition: User.php:209
checkPhone()
Check that a phone number is unique.
Definition: User.php:1073
static login($user, $sendAuthHeader)
Logs the given user into the system.
Definition: Tilmeld.php:513
saveSkipAC()
This should never be accessible on the client.
Definition: User.php:1469
getDescendantGroups()
Get the user's group descendants.
Definition: User.php:513
checkUsername()
Check that a username is valid.
Definition: User.php:942
password($password)
Change the user's password.
Definition: User.php:921
checkEmail()
Check that an email is unique.
Definition: User.php:1022
delGroup($group)
Remove the user from a (secondary) group.
Definition: User.php:841
static logout()
Logs the current user out of the system.
Definition: Tilmeld.php:538
__construct($id=0)
Load a user.
Definition: User.php:142