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 }
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
$params
Saves global plugin settings.
Definition: save.php:13
Elgg registration action.
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
c Accompany it with the information you received as to the offer to distribute corresponding source complete source code means all the source code for all modules it plus any associated interface definition plus the scripts used to control compilation and installation of the executable as a special the source code distributed need not include anything that is normally and so on of the operating system on which the executable unless that component itself accompanies the executable If distribution of executable or object code is made by offering access to copy from a designated then offering equivalent access to copy the source code from the same place counts as distribution of the source even though third parties are not compelled to copy the source along with the object code You may not or distribute the Program except as expressly provided under this License Any attempt otherwise to sublicense or distribute the Program is void
Definition: LICENSE.txt:215
Represents a set of validated parameters.
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
Events service.
assertValidEmail(string $address, bool $assert_unregistered=false)
Simple validation of a email.
Definition: Accounts.php:314
$email
Definition: change_email.php:7
$username
Definition: delete.php:23
assertValidUsername(string $username, bool $assert_unregistered=false)
Simple function which ensures that a username contains only valid characters.
Definition: Accounts.php:192
Password service.
Could not register a new user for whatever reason.
assertValidPassword(string|array $password)
Simple validation of a password.
Definition: Accounts.php:264
$class
Definition: summary.php:44
elgg_strlen()
Wrapper function for mb_strlen().
Definition: mb_wrapper.php:53
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:254
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
$error
Bad request error.
Definition: 400.php:6
if(!$user||!$user->canEdit()) $password
$language
Definition: useradd.php:17
$user
Definition: ban.php:7
$results
Definition: content.php:22
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
$subtype
Definition: delete.php:22
Email service.
__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
User accounts service.
Definition: Accounts.php:20