Elgg  Version 5.1
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 
25  protected $config;
26 
30  protected $translator;
31 
35  protected $passwords;
36 
40  protected $events;
41 
45  protected $email;
46 
51 
62  public function __construct(
69  ) {
70  $this->config = $config;
71  $this->translator = $translator;
72  $this->passwords = $passwords;
73  $this->events = $events;
74  $this->email = $email;
75  $this->password_generator = $password_generator;
76  }
77 
91  public function validateAccountData(string $username, string|array $password, string $name, string $email, bool $allow_multiple_emails = false): ValidationResults {
92 
93  return elgg_call(ELGG_SHOW_DISABLED_ENTITIES, function () use ($username, $email, $password, $name, $allow_multiple_emails) {
95 
96  if (empty($name)) {
97  $error = $this->translator->translate('registration:noname');
98  $results->fail('name', $name, $error);
99  } else {
100  $results->pass('name', $name);
101  }
102 
103  try {
104  $this->assertValidEmail($email, !$allow_multiple_emails);
105 
106  $results->pass('email', $email);
107  } catch (RegistrationException $ex) {
108  $results->fail('email', $email, $ex->getMessage());
109  }
110 
111  try {
112  $this->assertValidPassword($password);
113 
114  $results->pass('password', $password);
115  } catch (RegistrationException $ex) {
116  $results->fail('password', $password, $ex->getMessage());
117  }
118 
119  try {
120  $this->assertValidUsername($username, true);
121 
122  $results->pass('username', $username);
123  } catch (RegistrationException $ex) {
124  $results->fail('username', $username, $ex->getMessage());
125  }
126 
127  return $results;
128  });
129  }
130 
143  public function assertValidAccountData(string $username, string|array $password, string $name, string $email, bool $allow_multiple_emails = false): void {
144 
145  $results = $this->validateAccountData($username, $password, $name, $email, $allow_multiple_emails);
146 
147  foreach ($results->all() as $result) {
148  if (!$result->isValid()) {
149  throw new RegistrationException($result->getError());
150  }
151  }
152  }
153 
170  public function register(array $params = []): \ElggUser {
171  $username = (string) elgg_extract('username', $params);
172  $password = (string) elgg_extract('password', $params);
173  $name = (string) elgg_extract('name', $params);
174  $email = (string) elgg_extract('email', $params);
175  $subtype = elgg_extract('subtype', $params);
176  $language = (string) elgg_extract('language', $params, $this->translator->getCurrentLanguage());
177  $allow_multiple_emails = (bool) elgg_extract('allow_multiple_emails', $params, false);
178  $validated = (bool) elgg_extract('validated', $params, true);
179 
180  $this->assertValidAccountData($username, $password, $name, $email, $allow_multiple_emails);
181 
182  // Create user
183  $constructor = \ElggUser::class;
184  if (isset($subtype)) {
186  if ($class && class_exists($class) && is_subclass_of($class, \ElggUser::class)) {
187  $constructor = $class;
188  }
189  }
190 
191  /* @var $user \ElggUser */
192  $user = new $constructor();
193 
194  if (isset($subtype)) {
195  $user->setSubtype($subtype);
196  }
197 
198  $user->username = $username;
199  $user->email = $email;
200  $user->name = $name;
201  $user->language = $language;
202 
203  if (!$user->save()) {
204  throw new RegistrationException($this->translator->translate('registerbad'));
205  }
206 
207  // doing this after save to prevent metadata save notices on unwritable metadata password_hash
208  $user->setPassword($password);
209 
210  // Turn on email notifications by default
211  $user->setNotificationSetting('email', true);
212 
213  if ($validated) {
214  $user->setValidationStatus(true, 'on_create');
215  }
216 
217  return $user;
218  }
219 
231  public function assertValidUsername(string $username, bool $assert_unregistered = false): void {
232 
233  if (elgg_strlen($username) < $this->config->minusername) {
234  $msg = $this->translator->translate('registration:usernametooshort', [$this->config->minusername]);
235  throw new RegistrationException($msg);
236  }
237 
238  // username in the database has a limit of 128 characters
239  if (strlen($username) > 128) {
240  $msg = $this->translator->translate('registration:usernametoolong', [128]);
241  throw new RegistrationException($msg);
242  }
243 
244  // Whitelist all supported route characters
245  // @see Elgg\Router\RouteRegistrationService::register()
246  // @link https://github.com/Elgg/Elgg/issues/12518
247  // @link https://github.com/Elgg/Elgg/issues/14239
248  $invalid_chars = [];
249  if (preg_match_all('/[^\p{L}\p{M}\p{Nd}._-]+/iu', $username, $invalid_chars)) {
250  throw new RegistrationException($this->translator->translate('registration:invalidchars:route', [implode(',', $invalid_chars[0])]));
251  }
252 
253  // Belts and braces
254  // @todo Tidy into main unicode
255  $blacklist2 = '\'/\\"*& ?#%^(){}[]~?<>;|¬`@+=,:';
256 
257  $blacklist2 = $this->events->triggerResults(
258  'username:character_blacklist',
259  'user',
260  ['blacklist' => $blacklist2],
261  $blacklist2
262  );
263 
264  for ($n = 0; $n < elgg_strlen($blacklist2); $n++) {
265  if (elgg_strpos($username, $blacklist2[$n]) !== false) {
266  $msg = $this->translator->translate('registration:invalidchars', [$blacklist2[$n], $blacklist2]);
267  $msg = htmlspecialchars($msg, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
268  throw new RegistrationException($msg);
269  }
270  }
271 
272  $result = $this->events->triggerResults(
273  'registeruser:validate:username',
274  'all',
275  ['username' => $username],
276  true
277  );
278 
279  if (!$result) {
280  throw new RegistrationException($this->translator->translate('registration:usernamenotvalid'));
281  }
282 
283  if ($assert_unregistered) {
284  $exists = elgg_call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES, function () use ($username) {
285  return elgg_get_user_by_username($username);
286  });
287 
288  if ($exists instanceof \ElggUser) {
289  throw new RegistrationException($this->translator->translate('registration:userexists'));
290  }
291  }
292  }
293 
303  public function assertValidPassword(string|array $password): void {
304 
305  if (is_array($password)) {
306  list($password, $password2) = $password;
307 
308  if (empty($password) || empty($password2)) {
309  throw new RegistrationException(elgg_echo('RegistrationException:EmptyPassword'));
310  }
311 
312  if (strcmp($password, $password2) != 0) {
313  throw new RegistrationException(elgg_echo('RegistrationException:PasswordMismatch'));
314  }
315  }
316 
317  $result = $this->events->triggerResults(
318  'registeruser:validate:password',
319  'all',
320  ['password' => $password],
321  !empty($password)
322  );
323 
324  if (!$result) {
325  throw new RegistrationException($this->translator->translate('registration:passwordnotvalid'));
326  }
327  }
328 
338  public function assertCurrentPassword(\ElggUser $user, string $password): void {
339  if (!$this->passwords->verify($password, $user->password_hash)) {
340  throw new RegistrationException($this->translator->translate('LoginException:PasswordFailure'));
341  }
342  }
343 
353  public function assertValidEmail(string $address, bool $assert_unregistered = false): void {
354  if (!$this->isValidEmail($address)) {
355  throw new RegistrationException($this->translator->translate('registration:notemail'));
356  }
357 
358  $result = $this->events->triggerResults(
359  'registeruser:validate:email',
360  'all',
361  ['email' => $address],
362  true
363  );
364 
365  if (!$result) {
366  throw new RegistrationException($this->translator->translate('registration:emailnotvalid'));
367  }
368 
369  if ($assert_unregistered) {
370  $exists = elgg_call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES, function () use ($address) {
371  return elgg_get_user_by_email($address);
372  });
373 
374  if ($exists instanceof \ElggUser) {
375  throw new RegistrationException($this->translator->translate('registration:dupeemail'));
376  }
377  }
378  }
379 
387  public function isValidEmail(string $address): bool {
388  return filter_var($address, FILTER_VALIDATE_EMAIL) === $address;
389  }
390 
400  public function requestNewEmailValidation(\ElggUser $user, string $email): bool {
401  if (!$this->isValidEmail($email)) {
402  throw new InvalidArgumentException($this->translator->translate('registration:notemail'));
403  }
404 
405  $site = elgg_get_site_entity();
406 
407  $user->new_email = $email;
408 
409  $url = elgg_generate_url('account:email:confirm', [
410  'guid' => $user->guid,
411  ]);
412  $url = elgg_http_get_signed_url($url, '+1 hour');
413 
414  $notification = Email::factory([
415  'from' => $site,
416  'to' => new Address($email, $user->getDisplayName()),
417  'subject' => $this->translator->translate('email:request:email:subject', [], $user->getLanguage()),
418  'body' => $this->translator->translate('email:request:email:body', [
419  $site->getDisplayName(),
420  $url,
421  ], $user->getLanguage()),
422  ]);
423 
424  return $this->email->send($notification);
425  }
426 
435  public function registerAuthenticationFailure(\ElggUser $user): void {
436  $fails = (int) $user->authentication_failures;
437  $fails++;
438 
439  $user->authentication_failures = $fails;
440  $user->{"authentication_failure_{$fails}"} = time();
441  }
442 
451  public function resetAuthenticationFailures(\ElggUser $user): void {
452  $fails = (int) $user->authentication_failures;
453  if (empty($fails)) {
454  return;
455  }
456 
457  for ($n = 1; $n <= $fails; $n++) {
458  unset($user->{"authentication_failure_{$n}"});
459  }
460 
461  unset($user->authentication_failures);
462  }
463 
474  public function isAuthenticationFailureLimitReached(\ElggUser $user, int $limit = null, int $lifetime = null): bool {
475  $limit = $limit ?? $this->config->authentication_failures_limit;
476  $lifetime = $lifetime ?? $this->config->authentication_failures_lifetime;
477 
478  $fails = (int) $user->authentication_failures;
479  if (empty($fails) || $fails < $limit) {
480  return false;
481  }
482 
483  $failure_count = 0;
484  $min_time = time() - $lifetime;
485  for ($n = $fails; $n > 0; $n--) {
486  $failure_timestamp = $user->{"authentication_failure_{$n}"};
487  if ($failure_timestamp > $min_time) {
488  $failure_count++;
489  }
490 
491  if ($failure_count === $limit) {
492  // Limit reached
493  return true;
494  }
495  }
496 
497  return false;
498  }
499 }
elgg_call(int $flags, Closure $closure)
Calls a callable autowiring the arguments using public DI services and applying logic based on flags...
Definition: elgglib.php:299
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:18
$params
Saves global plugin settings.
Definition: save.php:13
__construct(Config $config, Translator $translator, PasswordService $passwords, EventsService $events, EmailService $email, PasswordGeneratorService $password_generator)
Constructor.
Definition: Accounts.php:62
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:91
Events service.
assertValidEmail(string $address, bool $assert_unregistered=false)
Simple validation of a email.
Definition: Accounts.php:353
$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:231
Password service.
Could not register a new user for whatever reason.
assertValidPassword(string|array $password)
Simple validation of a password.
Definition: Accounts.php:303
$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
$error
Bad request error.
Definition: 400.php:6
if(!$user||!$user->canEdit()) $password
const ELGG_SHOW_DISABLED_ENTITIES
Definition: constants.php:132
$language
Definition: useradd.php:17
$site email
Definition: settings.php:16
$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:143
$subtype
Definition: delete.php:23
Email service.
User accounts service.
Definition: Accounts.php:20