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