1<?php 2 3namespace Elgg\Users; 4 5use Elgg\Config; 6use Elgg\Database\UsersTable; 7use Elgg\I18n\Translator; 8use Elgg\PasswordService; 9use Elgg\PluginHooksService; 10use Elgg\Validation\ValidationResults; 11use ElggUser; 12use RegistrationException; 13use Elgg\Email; 14use Elgg\Email\Address; 15use Elgg\EmailService; 16use Elgg\Security\PasswordGeneratorService; 17 18/** 19 * User accounts service 20 */ 21class Accounts { 22 23 /** 24 * @var Config 25 */ 26 protected $config; 27 28 /** 29 * @var Translator 30 */ 31 protected $translator; 32 33 /** 34 * @var PasswordService 35 */ 36 protected $passwords; 37 38 /** 39 * @var UsersTable 40 */ 41 protected $users; 42 43 /** 44 * @var PluginHooksService 45 */ 46 protected $hooks; 47 48 /** 49 * @var EmailService 50 */ 51 protected $email; 52 53 /** 54 * @var PasswordGeneratorService 55 */ 56 protected $password_generator; 57 58 /** 59 * Constructor 60 * 61 * @param Config $config Config 62 * @param Translator $translator Translator 63 * @param PasswordService $passwords Passwords 64 * @param UsersTable $users Users table 65 * @param PluginHooksService $hooks Plugin hooks service 66 * @param EmailService $email Email service 67 * @param PasswordGeneratorService $password_generator Password generator service 68 */ 69 public function __construct( 70 Config $config, 71 Translator $translator, 72 PasswordService $passwords, 73 UsersTable $users, 74 PluginHooksService $hooks, 75 EmailService $email, 76 PasswordGeneratorService $password_generator 77 ) { 78 $this->config = $config; 79 $this->translator = $translator; 80 $this->passwords = $passwords; 81 $this->users = $users; 82 $this->hooks = $hooks; 83 $this->email = $email; 84 $this->password_generator = $password_generator; 85 } 86 87 /** 88 * Validate registration details to ensure they can be used to register a new user account 89 * 90 * @param string $username The username of the new user 91 * @param string|array $password The password 92 * Can be an array [$password, $oonfirm_password] 93 * @param string $name The user's display name 94 * @param string $email The user's email address 95 * @param bool $allow_multiple_emails Allow the same email address to be 96 * registered multiple times? 97 * 98 * @return ValidationResults 99 */ 100 public function validateAccountData($username, $password, $name, $email, $allow_multiple_emails = false) { 101 102 return elgg_call(ELGG_SHOW_DISABLED_ENTITIES, function () use ($username, $email, $password, $name, $allow_multiple_emails) { 103 $results = new ValidationResults(); 104 105 if (empty($name)) { 106 $error = $this->translator->translate('registration:noname'); 107 $results->fail('name', $name, $error); 108 } else { 109 $results->pass('name', $name); 110 } 111 112 try { 113 $this->assertValidEmail($email, !$allow_multiple_emails); 114 115 $results->pass('email', $email); 116 } catch (RegistrationException $ex) { 117 $results->fail('email', $email, $ex->getMessage()); 118 } 119 120 try { 121 $this->assertValidPassword($password); 122 123 $results->pass('password', $password); 124 } catch (RegistrationException $ex) { 125 $results->fail('password', $password, $ex->getMessage()); 126 } 127 128 try { 129 $this->assertValidUsername($username, true); 130 131 $results->pass('username', $username); 132 } catch (RegistrationException $ex) { 133 $results->fail('username', $username, $ex->getMessage()); 134 } 135 136 return $results; 137 }); 138 } 139 140 /** 141 * Assert that given registration details are valid and can be used to register the user 142 * 143 * @param string $username The username of the new user 144 * @param string $password The password 145 * @param string $name The user's display name 146 * @param string $email The user's email address 147 * @param bool $allow_multiple_emails Allow the same email address to be 148 * registered multiple times? 149 * 150 * @return void 151 * @throws RegistrationException 152 */ 153 public function assertValidAccountData($username, $password, $name, $email, $allow_multiple_emails = false) { 154 155 $results = $this->validateAccountData($username, $password, $name, $email, $allow_multiple_emails); 156 157 foreach ($results->all() as $result) { 158 if (!$result->isValid()) { 159 throw new RegistrationException($result->getError()); 160 } 161 } 162 163 } 164 165 /** 166 * Registers a user, returning false if the username already exists 167 * 168 * @param string $username The username of the new user 169 * @param string $password The password 170 * @param string $name The user's display name 171 * @param string $email The user's email address 172 * @param bool $allow_multiple_emails Allow the same email address to be 173 * registered multiple times? 174 * @param string $subtype Subtype of the user entity 175 * 176 * @return int|false The new user's GUID; false on failure 177 * @throws RegistrationException 178 */ 179 public function register($username, $password, $name, $email, $allow_multiple_emails = false, $subtype = null) { 180 181 $this->assertValidAccountData($username, $password, $name, $email, $allow_multiple_emails); 182 183 // Create user 184 $constructor = ElggUser::class; 185 if (isset($subtype)) { 186 $class = elgg_get_entity_class('user', $subtype); 187 if ($class && class_exists($class) && is_subclass_of($class, ElggUser::class)) { 188 $constructor = $class; 189 } 190 } 191 192 $user = new $constructor(); 193 /* @var $user ElggUser */ 194 195 if (isset($subtype)) { 196 $user->subtype = $subtype; 197 } 198 199 $user->username = $username; 200 $user->email = $email; 201 $user->name = $name; 202 $user->access_id = ACCESS_PUBLIC; 203 $user->owner_guid = 0; // Users aren't owned by anyone, even if they are admin created. 204 $user->container_guid = 0; // Users aren't contained by anyone, even if they are admin created. 205 $user->language = $this->translator->getCurrentLanguage(); 206 207 if ($user->save() === false) { 208 return false; 209 } 210 211 // doing this after save to prevent metadata save notices on unwritable metadata password_hash 212 $user->setPassword($password); 213 214 // Turn on email notifications by default 215 $user->setNotificationSetting('email', true); 216 217 return $user->getGUID(); 218 } 219 220 /** 221 * Simple function which ensures that a username contains only valid characters. 222 * 223 * This should only permit chars that are valid on the file system as well. 224 * 225 * @param string $username Username 226 * @param bool $assert_unregistered Also assert that the username has not yet been registered 227 * 228 * @return void 229 * @throws RegistrationException 230 */ 231 public function assertValidUsername($username, $assert_unregistered = false) { 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 $whitelist = '/[\p{L}\p{M}\p{Nd}._-]+/'; 248 if (!preg_match_all($whitelist, $username)) { 249 throw new RegistrationException($this->translator->translate('registration:invalidchars')); 250 } 251 252 // Belts and braces 253 // @todo Tidy into main unicode 254 $blacklist2 = '\'/\\"*& ?#%^(){}[]~?<>;|¬`@+=,:'; 255 256 $blacklist2 = $this->hooks->trigger( 257 'username:character_blacklist', 258 'user', 259 ['blacklist' => $blacklist2], 260 $blacklist2 261 ); 262 263 for ($n = 0; $n < elgg_strlen($blacklist2); $n++) { 264 if (elgg_strpos($username, $blacklist2[$n]) !== false) { 265 $msg = $this->translator->translate('registration:invalidchars', [$blacklist2[$n], $blacklist2]); 266 $msg = htmlspecialchars($msg, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); 267 throw new RegistrationException($msg); 268 } 269 } 270 271 $result = $this->hooks->trigger( 272 'registeruser:validate:username', 273 'all', 274 ['username' => $username], 275 true 276 ); 277 278 if (!$result) { 279 throw new RegistrationException($this->translator->translate('registration:usernamenotvalid')); 280 } 281 282 if ($assert_unregistered) { 283 $exists = elgg_call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES, function () use ($username) { 284 return $this->users->getByUsername($username); 285 }); 286 287 if ($exists) { 288 throw new RegistrationException($this->translator->translate('registration:userexists')); 289 } 290 } 291 } 292 293 /** 294 * Simple validation of a password 295 * 296 * @param string|array $password Clear text password 297 * Can be an array [$password, $confirm_password] 298 * 299 * @return void 300 * @throws RegistrationException 301 */ 302 public function assertValidPassword($password) { 303 304 if (is_array($password)) { 305 list($password, $password2) = $password; 306 307 if (empty($password) || empty($password2)) { 308 throw new RegistrationException(elgg_echo('RegistrationException:EmptyPassword')); 309 } 310 311 if (strcmp($password, $password2) != 0) { 312 throw new RegistrationException(elgg_echo('RegistrationException:PasswordMismatch')); 313 } 314 } 315 316 $result = $this->hooks->trigger( 317 'registeruser:validate:password', 318 'all', 319 ['password' => $password], 320 true 321 ); 322 323 if (!$result) { 324 throw new RegistrationException($this->translator->translate('registration:passwordnotvalid')); 325 } 326 } 327 328 /** 329 * Assert that user can authenticate with the given password 330 * 331 * @param ElggUser $user User entity 332 * @param string $password Password 333 * 334 * @return void 335 * @throws RegistrationException 336 */ 337 public function assertCurrentPassword(ElggUser $user, $password) { 338 if (!$this->passwords->verify($password, $user->password_hash)) { 339 throw new RegistrationException($this->translator->translate('LoginException:PasswordFailure')); 340 } 341 } 342 343 /** 344 * Simple validation of a email. 345 * 346 * @param string $address Email address 347 * @param bool $assert_unregistered Also assert that the email address has not yet been used for a user account 348 * 349 * @return void 350 * @throws RegistrationException 351 */ 352 public function assertValidEmail($address, $assert_unregistered = false) { 353 if (!$this->isValidEmail($address)) { 354 throw new RegistrationException($this->translator->translate('registration:notemail')); 355 } 356 357 $result = $this->hooks->trigger( 358 'registeruser:validate:email', 359 'all', 360 ['email' => $address], 361 true 362 ); 363 364 if (!$result) { 365 throw new RegistrationException($this->translator->translate('registration:emailnotvalid')); 366 } 367 368 if ($assert_unregistered) { 369 $exists = elgg_call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES, function () use ($address) { 370 return $this->users->getByEmail($address); 371 }); 372 373 if ($exists) { 374 throw new RegistrationException($this->translator->translate('registration:dupeemail')); 375 } 376 } 377 } 378 379 /** 380 * Validates an email address. 381 * 382 * @param string $address Email address 383 * 384 * @return bool 385 */ 386 public function isValidEmail($address) { 387 return filter_var($address, FILTER_VALIDATE_EMAIL) === $address; 388 } 389 390 /** 391 * Send out an e-mail to the new email address the user wanted 392 * 393 * @param \ElggUser $user user with new e-mail address 394 * @param string $email E-mail address 395 * 396 * @return bool 397 * @throws \InvalidParameterException 398 */ 399 public function requestNewEmailValidation(\ElggUser $user, $email) { 400 if (!$this->isValidEmail($email)) { 401 throw new \InvalidParameterException($this->translator->translate('registration:notemail')); 402 } 403 404 $site = elgg_get_site_entity(); 405 406 $user->setPrivateSetting('new_email', $email); 407 408 $url = elgg_generate_url('account:email:confirm', [ 409 'guid' => $user->guid, 410 ]); 411 $url = elgg_http_get_signed_url($url, '+1 hour'); 412 413 $notification = Email::factory([ 414 'from' => $site, 415 'to' => new Address($email, $user->getDisplayName()), 416 'subject' => $this->translator->translate('email:request:email:subject', [], $user->getLanguage()), 417 'body' => $this->translator->translate('email:request:email:body', [ 418 $user->getDisplayName(), 419 $site->getDisplayName(), 420 $url, 421 ], $user->getLanguage()), 422 ]); 423 424 return $this->email->send($notification); 425 } 426} 427