Elgg  Version master
Accounts.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Users;
4 
5 use Elgg\Config;
6 use Elgg\Email;
16 
20 class Accounts {
21 
32  public function __construct(
33  protected Config $config,
34  protected Translator $translator,
35  protected PasswordService $passwords,
36  protected EventsService $events,
37  protected EmailService $email,
38  protected PasswordGeneratorService $password_generator
39  ) {
40  }
41 
55  public function validateAccountData(string $username, string|array $password, string $name, string $email, bool $allow_multiple_emails = false): ValidationResults {
57 
58  if (empty($name)) {
59  $error = $this->translator->translate('registration:noname');
60  $results->fail('name', $name, $error);
61  } else {
62  $results->pass('name', $name);
63  }
64 
65  try {
66  $this->assertValidEmail($email, !$allow_multiple_emails);
67 
68  $results->pass('email', $email);
69  } catch (RegistrationException $ex) {
70  $results->fail('email', $email, $ex->getMessage());
71  }
72 
73  try {
74  $this->assertValidPassword($password);
75 
76  $results->pass('password', $password);
77  } catch (RegistrationException $ex) {
78  $results->fail('password', $password, $ex->getMessage());
79  }
80 
81  try {
82  $this->assertValidUsername($username, true);
83 
84  $results->pass('username', $username);
85  } catch (RegistrationException $ex) {
86  $results->fail('username', $username, $ex->getMessage());
87  }
88 
89  return $results;
90  }
91 
104  public function assertValidAccountData(string $username, string|array $password, string $name, string $email, bool $allow_multiple_emails = false): void {
105 
106  $results = $this->validateAccountData($username, $password, $name, $email, $allow_multiple_emails);
107 
108  foreach ($results->all() as $result) {
109  if (!$result->isValid()) {
110  throw new RegistrationException($result->getError());
111  }
112  }
113  }
114 
131  public function register(array $params = []): \ElggUser {
132  $username = (string) elgg_extract('username', $params);
133  $password = (string) elgg_extract('password', $params);
134  $name = (string) elgg_extract('name', $params);
135  $email = (string) elgg_extract('email', $params);
136  $subtype = elgg_extract('subtype', $params);
137  $language = (string) elgg_extract('language', $params, $this->translator->getCurrentLanguage());
138  $allow_multiple_emails = (bool) elgg_extract('allow_multiple_emails', $params, false);
139  $validated = (bool) elgg_extract('validated', $params, true);
140 
141  $this->assertValidAccountData($username, $password, $name, $email, $allow_multiple_emails);
142 
143  // Create user
144  $constructor = \ElggUser::class;
145  if (isset($subtype)) {
147  if ($class && class_exists($class) && is_subclass_of($class, \ElggUser::class)) {
148  $constructor = $class;
149  }
150  }
151 
152  /* @var $user \ElggUser */
153  $user = new $constructor();
154 
155  if (isset($subtype)) {
156  $user->setSubtype($subtype);
157  }
158 
159  $user->username = $username;
160  $user->email = $email;
161  $user->name = $name;
162  $user->language = $language;
163 
164  if (!$user->save()) {
165  throw new RegistrationException($this->translator->translate('registerbad'));
166  }
167 
168  // doing this after save to prevent metadata save notices on unwritable metadata password_hash
169  $user->setPassword($password);
170 
171  // Turn on email notifications by default
172  $user->setNotificationSetting('email', true);
173 
174  if ($validated) {
175  $user->setValidationStatus(true, 'on_create');
176  }
177 
178  return $user;
179  }
180 
192  public function assertValidUsername(string $username, bool $assert_unregistered = false): void {
193 
194  if (elgg_strlen($username) < $this->config->minusername) {
195  $msg = $this->translator->translate('registration:usernametooshort', [$this->config->minusername]);
196  throw new RegistrationException($msg);
197  }
198 
199  // username in the database has a limit of 128 characters
200  if (strlen($username) > 128) {
201  $msg = $this->translator->translate('registration:usernametoolong', [128]);
202  throw new RegistrationException($msg);
203  }
204 
205  // Whitelist all supported route characters
206  // @see Elgg\Router\RouteRegistrationService::register()
207  // @link https://github.com/Elgg/Elgg/issues/12518
208  // @link https://github.com/Elgg/Elgg/issues/14239
209  $invalid_chars = [];
210  if (preg_match_all('/[^\p{L}\p{M}\p{Nd}._-]+/iu', $username, $invalid_chars)) {
211  throw new RegistrationException($this->translator->translate('registration:invalidchars:route', [implode(',', $invalid_chars[0])]));
212  }
213 
214  // Belts and braces
215  // @todo Tidy into main unicode
216  $blacklist2 = '\'/\\"*& ?#%^(){}[]~?<>;|¬`@+=,:';
217 
218  $blacklist2 = $this->events->triggerResults(
219  'username:character_blacklist',
220  'user',
221  ['blacklist' => $blacklist2],
222  $blacklist2
223  );
224 
225  for ($n = 0; $n < elgg_strlen($blacklist2); $n++) {
226  if (elgg_strpos($username, $blacklist2[$n]) !== false) {
227  $msg = $this->translator->translate('registration:invalidchars', [$blacklist2[$n], $blacklist2]);
228  $msg = htmlspecialchars($msg, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
229  throw new RegistrationException($msg);
230  }
231  }
232 
233  $result = $this->events->triggerResults(
234  'registeruser:validate:username',
235  'all',
236  ['username' => $username],
237  true
238  );
239 
240  if (!$result) {
241  throw new RegistrationException($this->translator->translate('registration:usernamenotvalid'));
242  }
243 
244  if ($assert_unregistered) {
245  $exists = elgg_call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES | ELGG_SHOW_DELETED_ENTITIES, function () use ($username) {
246  return elgg_get_user_by_username($username);
247  });
248 
249  if ($exists instanceof \ElggUser) {
250  throw new RegistrationException($this->translator->translate('registration:userexists'));
251  }
252  }
253  }
254 
264  public function assertValidPassword(string|array $password): void {
265 
266  if (is_array($password)) {
267  list($password, $password2) = $password;
268 
269  if (empty($password) || empty($password2)) {
270  throw new RegistrationException(elgg_echo('RegistrationException:EmptyPassword'));
271  }
272 
273  if (strcmp($password, $password2) != 0) {
274  throw new RegistrationException(elgg_echo('RegistrationException:PasswordMismatch'));
275  }
276  }
277 
278  $result = $this->events->triggerResults(
279  'registeruser:validate:password',
280  'all',
281  ['password' => $password],
282  !empty($password)
283  );
284 
285  if (!$result) {
286  throw new RegistrationException($this->translator->translate('registration:passwordnotvalid'));
287  }
288  }
289 
299  public function assertCurrentPassword(\ElggUser $user, string $password): void {
300  if (!$this->passwords->verify($password, $user->password_hash)) {
301  throw new RegistrationException($this->translator->translate('LoginException:PasswordFailure'));
302  }
303  }
304 
314  public function assertValidEmail(string $address, bool $assert_unregistered = false): void {
315  if (!$this->isValidEmail($address)) {
316  throw new RegistrationException($this->translator->translate('registration:notemail'));
317  }
318 
319  $result = $this->events->triggerResults(
320  'registeruser:validate:email',
321  'all',
322  ['email' => $address],
323  true
324  );
325 
326  if (!$result) {
327  throw new RegistrationException($this->translator->translate('registration:emailnotvalid'));
328  }
329 
330  if ($assert_unregistered) {
331  $exists = elgg_call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES | ELGG_SHOW_DELETED_ENTITIES, function () use ($address) {
332  return elgg_get_user_by_email($address);
333  });
334 
335  if ($exists instanceof \ElggUser) {
336  throw new RegistrationException($this->translator->translate('registration:dupeemail'));
337  }
338  }
339  }
340 
348  public function isValidEmail(string $address): bool {
349  return filter_var($address, FILTER_VALIDATE_EMAIL) === $address;
350  }
351 
361  public function requestNewEmailValidation(\ElggUser $user, string $email): bool {
362  if (!$this->isValidEmail($email)) {
363  throw new InvalidArgumentException($this->translator->translate('registration:notemail'));
364  }
365 
366  $site = elgg_get_site_entity();
367 
368  $user->new_email = $email;
369 
370  $url = elgg_generate_url('account:email:confirm', [
371  'guid' => $user->guid,
372  ]);
373  $url = elgg_http_get_signed_url($url, '+1 hour');
374 
375  $notification = Email::factory([
376  'from' => $site,
377  'to' => new Address($email, $user->getDisplayName()),
378  'subject' => $this->translator->translate('email:request:email:subject', [], $user->getLanguage()),
379  'body' => $this->translator->translate('email:request:email:body', [
380  $site->getDisplayName(),
381  $url,
382  ], $user->getLanguage()),
383  ]);
384 
385  return $this->email->send($notification);
386  }
387 
396  public function registerAuthenticationFailure(\ElggUser $user): void {
397  $fails = (int) $user->authentication_failures;
398  $fails++;
399 
400  $user->authentication_failures = $fails;
401  $user->{"authentication_failure_{$fails}"} = time();
402  }
403 
412  public function resetAuthenticationFailures(\ElggUser $user): void {
413  $fails = (int) $user->authentication_failures;
414  if (empty($fails)) {
415  return;
416  }
417 
418  for ($n = 1; $n <= $fails; $n++) {
419  unset($user->{"authentication_failure_{$n}"});
420  }
421 
422  unset($user->authentication_failures);
423  }
424 
435  public function isAuthenticationFailureLimitReached(\ElggUser $user, int $limit = null, int $lifetime = null): bool {
436  $limit = $limit ?? $this->config->authentication_failures_limit;
437  $lifetime = $lifetime ?? $this->config->authentication_failures_lifetime;
438 
439  $fails = (int) $user->authentication_failures;
440  if (empty($fails) || $fails < $limit) {
441  return false;
442  }
443 
444  $failure_count = 0;
445  $min_time = time() - $lifetime;
446  for ($n = $fails; $n > 0; $n--) {
447  $failure_timestamp = $user->{"authentication_failure_{$n}"};
448  if ($failure_timestamp > $min_time) {
449  $failure_count++;
450  }
451 
452  if ($failure_count === $limit) {
453  // Limit reached
454  return true;
455  }
456  }
457 
458  return false;
459  }
460 }
$error
Bad request error.
Definition: 400.php:6
$email
Definition: change_email.php:7
$username
Definition: delete.php:23
if(! $user||! $user->canDelete()) $name
Definition: delete.php:22
$subtype
Definition: delete.php:22
$params
Saves global plugin settings.
Definition: save.php:13
$language
Definition: useradd.php:17
$class
Definition: summary.php:44
$user
Definition: ban.php:7
Email service.
Email address.
Definition: Address.php:15
Events service.
Could not register a new user for whatever reason.
Exception thrown if an argument is not of the expected type.
Password service.
User accounts service.
Definition: Accounts.php:20
assertValidUsername(string $username, bool $assert_unregistered=false)
Simple function which ensures that a username contains only valid characters.
Definition: Accounts.php:192
validateAccountData(string $username, string|array $password, string $name, string $email, bool $allow_multiple_emails=false)
Validate registration details to ensure they can be used to register a new user account.
Definition: Accounts.php:55
__construct(protected Config $config, protected Translator $translator, protected PasswordService $passwords, protected EventsService $events, protected EmailService $email, protected PasswordGeneratorService $password_generator)
Constructor.
Definition: Accounts.php:32
assertValidPassword(string|array $password)
Simple validation of a password.
Definition: Accounts.php:264
assertValidAccountData(string $username, string|array $password, string $name, string $email, bool $allow_multiple_emails=false)
Assert that given registration details are valid and can be used to register the user.
Definition: Accounts.php:104
assertValidEmail(string $address, bool $assert_unregistered=false)
Simple validation of a email.
Definition: Accounts.php:314
Represents a set of validated parameters.
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:256
elgg_get_entity_class(string $type, string $subtype)
Return the class name registered as a constructor for an entity of a given type and subtype.
Definition: entities.php:21
elgg_strlen()
Wrapper function for mb_strlen().
Definition: mb_wrapper.php:53
if(! $user||! $user->canEdit()) $password
if(parse_url(elgg_get_site_url(), PHP_URL_PATH) !=='/') if(file_exists(elgg_get_root_path() . 'robots.txt'))
Set robots.txt.
Definition: robots.php:10
$results
Definition: content.php:22