1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2018 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16declare(strict_types=1); 17 18namespace Fisharebest\Webtrees; 19 20use stdClass; 21 22/** 23 * Provide an interface to the wt_user table. 24 */ 25class User 26{ 27 /** @var int The primary key of this user. */ 28 private $user_id; 29 30 /** @var string The login name of this user. */ 31 private $user_name; 32 33 /** @var string The real (display) name of this user. */ 34 private $real_name; 35 36 /** @var string The email address of this user. */ 37 private $email; 38 39 /** @var string[] Cached copy of the wt_user_setting table. */ 40 private $preferences = []; 41 42 /** @var User[]|null[] Only fetch users from the database once. */ 43 private static $cache = []; 44 45 /** 46 * Create a new user object from a row in the database. 47 * 48 * @param stdClass $user A row from the wt_user table 49 */ 50 public function __construct(stdClass $user) 51 { 52 $this->user_id = (int) $user->user_id; 53 $this->user_name = $user->user_name; 54 $this->real_name = $user->real_name; 55 $this->email = $user->email; 56 } 57 58 /** 59 * Create a new user. 60 * The calling code needs to check for duplicates identifiers before calling 61 * this function. 62 * 63 * @param string $user_name 64 * @param string $real_name 65 * @param string $email 66 * @param string $password 67 * 68 * @return User 69 */ 70 public static function create($user_name, $real_name, $email, $password): User 71 { 72 Database::prepare( 73 "INSERT INTO `##user` (user_name, real_name, email, password) VALUES (:user_name, :real_name, :email, :password)" 74 )->execute([ 75 'user_name' => $user_name, 76 'real_name' => $real_name, 77 'email' => $email, 78 'password' => password_hash($password, PASSWORD_DEFAULT), 79 ]); 80 81 // Set default blocks for this user 82 $user = self::findByIdentifier($user_name); 83 Database::prepare( 84 "INSERT INTO `##block` (`user_id`, `location`, `block_order`, `module_name`)" . 85 " SELECT :user_id , `location`, `block_order`, `module_name` FROM `##block` WHERE `user_id` = -1" 86 )->execute([ 87 'user_id' => $user->getUserId(), 88 ]); 89 90 return $user; 91 } 92 93 /** 94 * Delete a user 95 * 96 * @return void 97 */ 98 public function delete() 99 { 100 // Don't delete the logs. 101 Database::prepare("UPDATE `##log` SET user_id=NULL WHERE user_id =?")->execute([$this->user_id]); 102 // Take over the user’s pending changes. (What else could we do with them?) 103 Database::prepare("DELETE FROM `##change` WHERE user_id=? AND status='rejected'")->execute([$this->user_id]); 104 Database::prepare("UPDATE `##change` SET user_id=? WHERE user_id=?")->execute([ 105 Auth::id(), 106 $this->user_id, 107 ]); 108 Database::prepare("DELETE `##block_setting` FROM `##block_setting` JOIN `##block` USING (block_id) WHERE user_id=?")->execute([$this->user_id]); 109 Database::prepare("DELETE FROM `##block` WHERE user_id=?")->execute([$this->user_id]); 110 Database::prepare("DELETE FROM `##user_gedcom_setting` WHERE user_id=?")->execute([$this->user_id]); 111 Database::prepare("DELETE FROM `##gedcom_setting` WHERE setting_value=? AND setting_name IN ('CONTACT_USER_ID', 'WEBMASTER_USER_ID')")->execute([(string) $this->user_id]); 112 Database::prepare("DELETE FROM `##user_setting` WHERE user_id=?")->execute([$this->user_id]); 113 Database::prepare("DELETE FROM `##message` WHERE user_id=?")->execute([$this->user_id]); 114 Database::prepare("DELETE FROM `##user` WHERE user_id=?")->execute([$this->user_id]); 115 } 116 117 /** 118 * Find the user with a specified user_id. 119 * 120 * @param int|null $user_id 121 * 122 * @return User|null 123 */ 124 public static function find($user_id) 125 { 126 if (!array_key_exists($user_id, self::$cache)) { 127 $row = Database::prepare( 128 "SELECT user_id, user_name, real_name, email FROM `##user` WHERE user_id = ?" 129 )->execute([$user_id])->fetchOneRow(); 130 if ($row) { 131 self::$cache[$user_id] = new self($row); 132 } else { 133 self::$cache[$user_id] = null; 134 } 135 } 136 137 return self::$cache[$user_id]; 138 } 139 140 /** 141 * Find the user with a specified email address. 142 * 143 * @param string $email 144 * 145 * @return User|null 146 */ 147 public static function findByEmail($email) 148 { 149 $user_id = (int) Database::prepare( 150 "SELECT user_id FROM `##user` WHERE email = :email" 151 )->execute([ 152 'email' => $email, 153 ])->fetchOne(); 154 155 return self::find($user_id); 156 } 157 158 /** 159 * Find the user with a specified user_name or email address. 160 * 161 * @param string $identifier 162 * 163 * @return User|null 164 */ 165 public static function findByIdentifier($identifier) 166 { 167 $user_id = (int) Database::prepare( 168 "SELECT user_id FROM `##user` WHERE ? IN (user_name, email)" 169 )->execute([$identifier])->fetchOne(); 170 171 return self::find($user_id); 172 } 173 174 /** 175 * Find the user with a specified genealogy record. 176 * 177 * @param Individual $individual 178 * 179 * @return User|null 180 */ 181 public static function findByIndividual(Individual $individual) 182 { 183 $user_id = (int) Database::prepare( 184 "SELECT user_id" . 185 " FROM `##user_gedcom_setting`" . 186 " WHERE gedcom_id = :tree_id AND setting_name = 'gedcomid' AND setting_value = :xref" 187 )->execute([ 188 'tree_id' => $individual->tree()->id(), 189 'xref' => $individual->xref(), 190 ])->fetchOne(); 191 192 return self::find($user_id); 193 } 194 195 /** 196 * Find the user with a specified user_name. 197 * 198 * @param string $user_name 199 * 200 * @return User|null 201 */ 202 public static function findByUserName($user_name) 203 { 204 $user_id = (int) Database::prepare( 205 "SELECT user_id FROM `##user` WHERE user_name = :user_name" 206 )->execute([ 207 'user_name' => $user_name, 208 ])->fetchOne(); 209 210 return self::find($user_id); 211 } 212 213 /** 214 * Get a list of all users. 215 * 216 * @return User[] 217 */ 218 public static function all(): array 219 { 220 $rows = Database::prepare( 221 "SELECT user_id, user_name, real_name, email" . 222 " FROM `##user`" . 223 " WHERE user_id > 0" . 224 " ORDER BY real_name" 225 )->fetchAll(); 226 227 return array_map(function (stdClass $row): User { 228 return new static($row); 229 }, $rows); 230 } 231 232 /** 233 * Get a list of all administrators. 234 * 235 * @return User[] 236 */ 237 public static function administrators(): array 238 { 239 $rows = Database::prepare( 240 "SELECT user_id, user_name, real_name, email" . 241 " FROM `##user`" . 242 " JOIN `##user_setting` USING (user_id)" . 243 " WHERE user_id > 0 AND setting_name = 'canadmin' AND setting_value = '1'" . 244 " ORDER BY real_name" 245 )->fetchAll(); 246 247 return array_map(function (stdClass $row): User { 248 return new static($row); 249 }, $rows); 250 } 251 252 /** 253 * Validate a supplied password 254 * 255 * @param string $password 256 * 257 * @return bool 258 */ 259 public function checkPassword(string $password): bool 260 { 261 $password_hash = Database::prepare( 262 "SELECT password FROM `##user` WHERE user_id = ?" 263 )->execute([$this->user_id])->fetchOne(); 264 265 if ($password_hash !== null && password_verify($password, $password_hash)) { 266 if (password_needs_rehash($password_hash, PASSWORD_DEFAULT)) { 267 $this->setPassword($password); 268 } 269 270 return true; 271 } 272 273 return false; 274 } 275 276 /** 277 * Get a list of all managers. 278 * 279 * @return User[] 280 */ 281 public static function managers(): array 282 { 283 $rows = Database::prepare( 284 "SELECT user_id, user_name, real_name, email" . 285 " FROM `##user` JOIN `##user_gedcom_setting` USING (user_id)" . 286 " WHERE setting_name = 'canedit' AND setting_value='admin'" . 287 " GROUP BY user_id, real_name" . 288 " ORDER BY real_name" 289 )->fetchAll(); 290 291 return array_map(function (stdClass $row): User { 292 return new static($row); 293 }, $rows); 294 } 295 296 /** 297 * Get a list of all moderators. 298 * 299 * @return User[] 300 */ 301 public static function moderators(): array 302 { 303 $rows = Database::prepare( 304 "SELECT user_id, user_name, real_name, email" . 305 " FROM `##user` JOIN `##user_gedcom_setting` USING (user_id)" . 306 " WHERE setting_name = 'canedit' AND setting_value='accept'" . 307 " GROUP BY user_id, real_name" . 308 " ORDER BY real_name" 309 )->fetchAll(); 310 311 return array_map(function (stdClass $row): User { 312 return new static($row); 313 }, $rows); 314 } 315 316 /** 317 * Get a list of all verified users. 318 * 319 * @return User[] 320 */ 321 public static function unapproved(): array 322 { 323 $rows = Database::prepare( 324 "SELECT user_id, user_name, real_name, email" . 325 " FROM `##user` JOIN `##user_setting` USING (user_id)" . 326 " WHERE setting_name = 'verified_by_admin' AND setting_value = '0'" . 327 " ORDER BY real_name" 328 )->fetchAll(); 329 330 return array_map(function (stdClass $row): User { 331 return new static($row); 332 }, $rows); 333 } 334 335 /** 336 * Get a list of all verified users. 337 * 338 * @return User[] 339 */ 340 public static function unverified(): array 341 { 342 $rows = Database::prepare( 343 "SELECT user_id, user_name, real_name, email" . 344 " FROM `##user` JOIN `##user_setting` USING (user_id)" . 345 " WHERE setting_name = 'verified' AND setting_value = '0'" . 346 " ORDER BY real_name" 347 )->fetchAll(); 348 349 return array_map(function (stdClass $row): User { 350 return new static($row); 351 }, $rows); 352 } 353 354 /** 355 * Get a list of all users who are currently logged in. 356 * 357 * @return User[] 358 */ 359 public static function allLoggedIn(): array 360 { 361 $rows = Database::prepare( 362 "SELECT DISTINCT user_id, user_name, real_name, email" . 363 " FROM `##user`" . 364 " JOIN `##session` USING (user_id)" 365 )->fetchAll(); 366 367 return array_map(function (stdClass $row): User { 368 return new static($row); 369 }, $rows); 370 } 371 372 /** 373 * Get the numeric ID for this user. 374 * 375 * @return int 376 */ 377 public function getUserId(): int 378 { 379 return $this->user_id; 380 } 381 382 /** 383 * Get the login name for this user. 384 * 385 * @return string 386 */ 387 public function getUserName(): string 388 { 389 return $this->user_name; 390 } 391 392 /** 393 * Set the login name for this user. 394 * 395 * @param string $user_name 396 * 397 * @return $this 398 */ 399 public function setUserName($user_name): self 400 { 401 if ($this->user_name !== $user_name) { 402 $this->user_name = $user_name; 403 Database::prepare( 404 "UPDATE `##user` SET user_name = ? WHERE user_id = ?" 405 )->execute([ 406 $user_name, 407 $this->user_id, 408 ]); 409 } 410 411 return $this; 412 } 413 414 /** 415 * Get the real name of this user. 416 * 417 * @return string 418 */ 419 public function getRealName(): string 420 { 421 return $this->real_name; 422 } 423 424 /** 425 * Set the real name of this user. 426 * 427 * @param string $real_name 428 * 429 * @return User 430 */ 431 public function setRealName($real_name): User 432 { 433 if ($this->real_name !== $real_name) { 434 $this->real_name = $real_name; 435 Database::prepare( 436 "UPDATE `##user` SET real_name = ? WHERE user_id = ?" 437 )->execute([ 438 $real_name, 439 $this->user_id, 440 ]); 441 } 442 443 return $this; 444 } 445 446 /** 447 * Get the email address of this user. 448 * 449 * @return string 450 */ 451 public function getEmail(): string 452 { 453 return $this->email; 454 } 455 456 /** 457 * Set the email address of this user. 458 * 459 * @param string $email 460 * 461 * @return User 462 */ 463 public function setEmail($email): User 464 { 465 if ($this->email !== $email) { 466 $this->email = $email; 467 Database::prepare( 468 "UPDATE `##user` SET email = ? WHERE user_id = ?" 469 )->execute([ 470 $email, 471 $this->user_id, 472 ]); 473 } 474 475 return $this; 476 } 477 478 /** 479 * Set the password of this user. 480 * 481 * @param string $password 482 * 483 * @return User 484 */ 485 public function setPassword($password): User 486 { 487 Database::prepare( 488 "UPDATE `##user` SET password = :password WHERE user_id = :user_id" 489 )->execute([ 490 'password' => password_hash($password, PASSWORD_DEFAULT), 491 'user_id' => $this->user_id, 492 ]); 493 494 return $this; 495 } 496 497 /** 498 * Fetch a user option/setting from the wt_user_setting table. 499 * Since we'll fetch several settings for each user, and since there aren’t 500 * that many of them, fetch them all in one database query 501 * 502 * @param string $setting_name 503 * @param string $default 504 * 505 * @return string 506 */ 507 public function getPreference($setting_name, $default = ''): string 508 { 509 if (empty($this->preferences) && $this->user_id !== 0) { 510 $this->preferences = Database::prepare( 511 "SELECT setting_name, setting_value" . 512 " FROM `##user_setting`" . 513 " WHERE user_id = :user_id" 514 )->execute([ 515 'user_id' => $this->user_id, 516 ])->fetchAssoc(); 517 } 518 519 if (!array_key_exists($setting_name, $this->preferences)) { 520 $this->preferences[$setting_name] = $default; 521 } 522 523 return $this->preferences[$setting_name]; 524 } 525 526 /** 527 * Update a setting for the user. 528 * 529 * @param string $setting_name 530 * @param string $setting_value 531 * 532 * @return User 533 */ 534 public function setPreference($setting_name, $setting_value): User 535 { 536 if ($this->user_id !== 0 && $this->getPreference($setting_name) !== $setting_value) { 537 Database::prepare( 538 "REPLACE INTO `##user_setting` (user_id, setting_name, setting_value) VALUES (?, ?, LEFT(?, 255))" 539 )->execute([ 540 $this->user_id, 541 $setting_name, 542 $setting_value, 543 ]); 544 545 $this->preferences[$setting_name] = $setting_value; 546 } 547 548 return $this; 549 } 550} 551