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