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