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[] 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) 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->getTree()->getTreeId(), 189 'xref' => $individual->getXref(), 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 * Find the latest user to register. 215 * 216 * @return User|null 217 */ 218 public static function findLatestToRegister() 219 { 220 $user_id = (int) Database::prepare( 221 "SELECT u.user_id" . 222 " FROM `##user` u" . 223 " LEFT JOIN `##user_setting` us ON (u.user_id=us.user_id AND us.setting_name='reg_timestamp') " . 224 " ORDER BY us.setting_value DESC LIMIT 1" 225 )->execute()->fetchOne(); 226 227 return self::find($user_id); 228 } 229 230 /** 231 * Get a list of all users. 232 * 233 * @return User[] 234 */ 235 public static function all(): array 236 { 237 $rows = Database::prepare( 238 "SELECT user_id, user_name, real_name, email" . 239 " FROM `##user`" . 240 " WHERE user_id > 0" . 241 " ORDER BY real_name" 242 )->fetchAll(); 243 244 return array_map(function (stdClass $row): User { 245 return new static($row); 246 }, $rows); 247 } 248 249 /** 250 * Get a list of all administrators. 251 * 252 * @return User[] 253 */ 254 public static function administrators(): array 255 { 256 $rows = Database::prepare( 257 "SELECT user_id, user_name, real_name, email" . 258 " FROM `##user`" . 259 " JOIN `##user_setting` USING (user_id)" . 260 " WHERE user_id > 0 AND setting_name = 'canadmin' AND setting_value = '1'" . 261 " ORDER BY real_name" 262 )->fetchAll(); 263 264 return array_map(function (stdClass $row): User { 265 return new static($row); 266 }, $rows); 267 } 268 269 /** 270 * Validate a supplied password 271 * 272 * @param string $password 273 * 274 * @return bool 275 */ 276 public function checkPassword(string $password): bool 277 { 278 $password_hash = Database::prepare( 279 "SELECT password FROM `##user` WHERE user_id = ?" 280 )->execute([$this->user_id])->fetchOne(); 281 282 if (password_verify($password, $password_hash)) { 283 if (password_needs_rehash($password_hash, PASSWORD_DEFAULT)) { 284 $this->setPassword($password); 285 } 286 287 return true; 288 } else { 289 return false; 290 } 291 } 292 293 /** 294 * Get a list of all managers. 295 * 296 * @return User[] 297 */ 298 public static function managers(): array 299 { 300 $rows = Database::prepare( 301 "SELECT user_id, user_name, real_name, email" . 302 " FROM `##user` JOIN `##user_gedcom_setting` USING (user_id)" . 303 " WHERE setting_name = 'canedit' AND setting_value='admin'" . 304 " GROUP BY user_id, real_name" . 305 " ORDER BY real_name" 306 )->fetchAll(); 307 308 return array_map(function (stdClass $row): User { 309 return new static($row); 310 }, $rows); 311 } 312 313 /** 314 * Get a list of all moderators. 315 * 316 * @return User[] 317 */ 318 public static function moderators(): array 319 { 320 $rows = Database::prepare( 321 "SELECT user_id, user_name, real_name, email" . 322 " FROM `##user` JOIN `##user_gedcom_setting` USING (user_id)" . 323 " WHERE setting_name = 'canedit' AND setting_value='accept'" . 324 " GROUP BY user_id, real_name" . 325 " ORDER BY real_name" 326 )->fetchAll(); 327 328 return array_map(function (stdClass $row): User { 329 return new static($row); 330 }, $rows); 331 } 332 333 /** 334 * Get a list of all verified users. 335 * 336 * @return User[] 337 */ 338 public static function unapproved(): array 339 { 340 $rows = Database::prepare( 341 "SELECT user_id, user_name, real_name, email" . 342 " FROM `##user` JOIN `##user_setting` USING (user_id)" . 343 " WHERE setting_name = 'verified_by_admin' AND setting_value = '0'" . 344 " ORDER BY real_name" 345 )->fetchAll(); 346 347 return array_map(function (stdClass $row): User { 348 return new static($row); 349 }, $rows); 350 } 351 352 /** 353 * Get a list of all verified users. 354 * 355 * @return User[] 356 */ 357 public static function unverified(): array 358 { 359 $rows = Database::prepare( 360 "SELECT user_id, user_name, real_name, email" . 361 " FROM `##user` JOIN `##user_setting` USING (user_id)" . 362 " WHERE setting_name = 'verified' AND setting_value = '0'" . 363 " ORDER BY real_name" 364 )->fetchAll(); 365 366 return array_map(function (stdClass $row): User { 367 return new static($row); 368 }, $rows); 369 } 370 371 /** 372 * Get a list of all users who are currently logged in. 373 * 374 * @return User[] 375 */ 376 public static function allLoggedIn(): array 377 { 378 $rows = Database::prepare( 379 "SELECT DISTINCT user_id, user_name, real_name, email" . 380 " FROM `##user`" . 381 " JOIN `##session` USING (user_id)" 382 )->fetchAll(); 383 384 return array_map(function (stdClass $row): User { 385 return new static($row); 386 }, $rows); 387 } 388 389 /** 390 * Get the numeric ID for this user. 391 * 392 * @return int 393 */ 394 public function getUserId(): int 395 { 396 return $this->user_id; 397 } 398 399 /** 400 * Get the login name for this user. 401 * 402 * @return string 403 */ 404 public function getUserName() 405 { 406 return $this->user_name; 407 } 408 409 /** 410 * Set the login name for this user. 411 * 412 * @param string $user_name 413 * 414 * @return $this 415 */ 416 public function setUserName($user_name) 417 { 418 if ($this->user_name !== $user_name) { 419 $this->user_name = $user_name; 420 Database::prepare( 421 "UPDATE `##user` SET user_name = ? WHERE user_id = ?" 422 )->execute([ 423 $user_name, 424 $this->user_id, 425 ]); 426 } 427 428 return $this; 429 } 430 431 /** 432 * Get the real name of this user. 433 * 434 * @return string 435 */ 436 public function getRealName() 437 { 438 return $this->real_name; 439 } 440 441 /** 442 * Set the real name of this user. 443 * 444 * @param string $real_name 445 * 446 * @return User 447 */ 448 public function setRealName($real_name) 449 { 450 if ($this->real_name !== $real_name) { 451 $this->real_name = $real_name; 452 Database::prepare( 453 "UPDATE `##user` SET real_name = ? WHERE user_id = ?" 454 )->execute([ 455 $real_name, 456 $this->user_id, 457 ]); 458 } 459 460 return $this; 461 } 462 463 /** 464 * Get the email address of this user. 465 * 466 * @return string 467 */ 468 public function getEmail() 469 { 470 return $this->email; 471 } 472 473 /** 474 * Set the email address of this user. 475 * 476 * @param string $email 477 * 478 * @return User 479 */ 480 public function setEmail($email) 481 { 482 if ($this->email !== $email) { 483 $this->email = $email; 484 Database::prepare( 485 "UPDATE `##user` SET email = ? WHERE user_id = ?" 486 )->execute([ 487 $email, 488 $this->user_id, 489 ]); 490 } 491 492 return $this; 493 } 494 495 /** 496 * Set the password of this user. 497 * 498 * @param string $password 499 * 500 * @return User 501 */ 502 public function setPassword($password) 503 { 504 Database::prepare( 505 "UPDATE `##user` SET password = :password WHERE user_id = :user_id" 506 )->execute([ 507 'password' => password_hash($password, PASSWORD_DEFAULT), 508 'user_id' => $this->user_id, 509 ]); 510 511 return $this; 512 } 513 514 /** 515 * Fetch a user option/setting from the wt_user_setting table. 516 * Since we'll fetch several settings for each user, and since there aren’t 517 * that many of them, fetch them all in one database query 518 * 519 * @param string $setting_name 520 * @param string $default 521 * 522 * @return string 523 */ 524 public function getPreference($setting_name, $default = '') 525 { 526 if (empty($this->preferences) && $this->user_id !== 0) { 527 $this->preferences = Database::prepare( 528 "SELECT setting_name, setting_value" . 529 " FROM `##user_setting`" . 530 " WHERE user_id = :user_id" 531 )->execute([ 532 'user_id' => $this->user_id, 533 ])->fetchAssoc(); 534 } 535 536 if (!array_key_exists($setting_name, $this->preferences)) { 537 $this->preferences[$setting_name] = $default; 538 } 539 540 return $this->preferences[$setting_name]; 541 } 542 543 /** 544 * Update a setting for the user. 545 * 546 * @param string $setting_name 547 * @param string $setting_value 548 * 549 * @return User 550 */ 551 public function setPreference($setting_name, $setting_value) 552 { 553 if ($this->user_id !== 0 && $this->getPreference($setting_name) !== $setting_value) { 554 Database::prepare( 555 "REPLACE INTO `##user_setting` (user_id, setting_name, setting_value) VALUES (?, ?, LEFT(?, 255))" 556 )->execute([ 557 $this->user_id, 558 $setting_name, 559 $setting_value, 560 ]); 561 562 $this->preferences[$setting_name] = $setting_value; 563 } 564 565 return $this; 566 } 567} 568