1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 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, just set the user to null. 105 DB::table('log') 106 ->where('user_id', '=', $this->user_id) 107 ->update(['user_id' => null]); 108 109 // Take over the user’s pending changes. (What else could we do with them?) 110 DB::table('change') 111 ->where('user_id', '=', $this->user_id) 112 ->where('status', '=', 'rejected') 113 ->delete(); 114 115 DB::table('change') 116 ->where('user_id', '=', $this->user_id) 117 ->update(['user_id' => Auth::id()]); 118 119 // Take over the user's contact details 120 DB::table('gedcom_setting') 121 ->where('setting_value', '=', $this->user_id) 122 ->whereIn('setting_name', ['CONTACT_USER_ID', 'WEBMASTER_USER_ID']) 123 ->update(['setting_value' => Auth::id()]); 124 125 // Delete settings and preferences 126 DB::table('block_setting') 127 ->join('block', 'block_setting.block_id', '=', 'block.block_id') 128 ->where('user_id', '=', $this->user_id) 129 ->delete(); 130 131 DB::table('block')->where('user_id', '=', $this->user_id)->delete(); 132 DB::table('user_gedcom_setting')->where('user_id', '=', $this->user_id)->delete(); 133 DB::table('user_setting')->where('user_id', '=', $this->user_id)->delete(); 134 DB::table('message')->where('user_id', '=', $this->user_id)->delete(); 135 DB::table('user')->where('user_id', '=', $this->user_id)->delete(); 136 137 unset(self::$cache[$this->user_id]); 138 } 139 140 /** 141 * Find the user with a specified user_id. 142 * 143 * @param int|null $user_id 144 * 145 * @return User|null 146 */ 147 public static function find($user_id) 148 { 149 if (!array_key_exists($user_id, self::$cache)) { 150 $row = DB::table('user') 151 ->where('user_id', '=', $user_id) 152 ->first(); 153 154 if ($row) { 155 self::$cache[$user_id] = new self($row); 156 } else { 157 self::$cache[$user_id] = null; 158 } 159 } 160 161 return self::$cache[$user_id]; 162 } 163 164 /** 165 * Find the user with a specified email address. 166 * 167 * @param string $email 168 * 169 * @return User|null 170 */ 171 public static function findByEmail($email) 172 { 173 $user_id = (int) DB::table('user') 174 ->where('email', '=', $email) 175 ->value('user_id'); 176 177 return self::find($user_id); 178 } 179 180 /** 181 * Find the user with a specified user_name or email address. 182 * 183 * @param string $identifier 184 * 185 * @return User|null 186 */ 187 public static function findByIdentifier($identifier) 188 { 189 $user_id = (int) DB::table('user') 190 ->where('user_name', '=', $identifier) 191 ->orWhere('email', '=', $identifier) 192 ->value('user_id'); 193 194 return self::find($user_id); 195 } 196 197 /** 198 * Find the user(s) with a specified genealogy record. 199 * 200 * @param Individual $individual 201 * 202 * @return User[] 203 */ 204 public static function findByIndividual(Individual $individual): array 205 { 206 $user_ids = DB::table('user_gedcom_setting') 207 ->where('gedcom_id', '=', $individual->tree()->id()) 208 ->where('setting_value', '=', $individual->xref()) 209 ->where('setting_name', '=', 'gedcomid') 210 ->pluck('user_id'); 211 212 return $user_ids->map(function (string $user_id): User { 213 return self::find((int) $user_id); 214 })->all(); 215 } 216 217 /** 218 * Find the user with a specified user_name. 219 * 220 * @param string $user_name 221 * 222 * @return User|null 223 */ 224 public static function findByUserName($user_name) 225 { 226 $user_id = (int) DB::table('user') 227 ->where('user_name', '=', $user_name) 228 ->value('user_id'); 229 230 return self::find($user_id); 231 } 232 233 /** 234 * Get a list of all users. 235 * 236 * @return User[] 237 */ 238 public static function all(): array 239 { 240 $rows = Database::prepare( 241 "SELECT user_id, user_name, real_name, email" . 242 " FROM `##user`" . 243 " WHERE user_id > 0" . 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 * Get a list of all administrators. 254 * 255 * @return User[] 256 */ 257 public static function administrators(): array 258 { 259 $rows = Database::prepare( 260 "SELECT user_id, user_name, real_name, email" . 261 " FROM `##user`" . 262 " JOIN `##user_setting` USING (user_id)" . 263 " WHERE user_id > 0 AND setting_name = 'canadmin' AND setting_value = '1'" . 264 " ORDER BY real_name" 265 )->fetchAll(); 266 267 return array_map(function (stdClass $row): User { 268 return new static($row); 269 }, $rows); 270 } 271 272 /** 273 * Validate a supplied password 274 * 275 * @param string $password 276 * 277 * @return bool 278 */ 279 public function checkPassword(string $password): bool 280 { 281 $password_hash = Database::prepare( 282 "SELECT password FROM `##user` WHERE user_id = ?" 283 )->execute([$this->user_id])->fetchOne(); 284 285 if ($password_hash !== null && password_verify($password, $password_hash)) { 286 if (password_needs_rehash($password_hash, PASSWORD_DEFAULT)) { 287 $this->setPassword($password); 288 } 289 290 return true; 291 } 292 293 return false; 294 } 295 296 /** 297 * Get a list of all managers. 298 * 299 * @return User[] 300 */ 301 public static function managers(): 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='admin'" . 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 moderators. 318 * 319 * @return User[] 320 */ 321 public static function moderators(): array 322 { 323 $rows = Database::prepare( 324 "SELECT user_id, user_name, real_name, email" . 325 " FROM `##user` JOIN `##user_gedcom_setting` USING (user_id)" . 326 " WHERE setting_name = 'canedit' AND setting_value='accept'" . 327 " GROUP BY user_id, real_name" . 328 " ORDER BY real_name" 329 )->fetchAll(); 330 331 return array_map(function (stdClass $row): User { 332 return new static($row); 333 }, $rows); 334 } 335 336 /** 337 * Get a list of all verified users. 338 * 339 * @return User[] 340 */ 341 public static function unapproved(): array 342 { 343 $rows = Database::prepare( 344 "SELECT user_id, user_name, real_name, email" . 345 " FROM `##user` JOIN `##user_setting` USING (user_id)" . 346 " WHERE setting_name = 'verified_by_admin' AND setting_value = '0'" . 347 " ORDER BY real_name" 348 )->fetchAll(); 349 350 return array_map(function (stdClass $row): User { 351 return new static($row); 352 }, $rows); 353 } 354 355 /** 356 * Get a list of all verified users. 357 * 358 * @return User[] 359 */ 360 public static function unverified(): array 361 { 362 $rows = Database::prepare( 363 "SELECT user_id, user_name, real_name, email" . 364 " FROM `##user` JOIN `##user_setting` USING (user_id)" . 365 " WHERE setting_name = 'verified' AND setting_value = '0'" . 366 " ORDER BY real_name" 367 )->fetchAll(); 368 369 return array_map(function (stdClass $row): User { 370 return new static($row); 371 }, $rows); 372 } 373 374 /** 375 * Get a list of all users who are currently logged in. 376 * 377 * @return User[] 378 */ 379 public static function allLoggedIn(): array 380 { 381 $rows = Database::prepare( 382 "SELECT DISTINCT user_id, user_name, real_name, email" . 383 " FROM `##user`" . 384 " JOIN `##session` USING (user_id)" 385 )->fetchAll(); 386 387 return array_map(function (stdClass $row): User { 388 return new static($row); 389 }, $rows); 390 } 391 392 /** 393 * Get the numeric ID for this user. 394 * 395 * @return int 396 */ 397 public function getUserId(): int 398 { 399 return $this->user_id; 400 } 401 402 /** 403 * Get the login name for this user. 404 * 405 * @return string 406 */ 407 public function getUserName(): string 408 { 409 return $this->user_name; 410 } 411 412 /** 413 * Set the login name for this user. 414 * 415 * @param string $user_name 416 * 417 * @return $this 418 */ 419 public function setUserName($user_name): self 420 { 421 if ($this->user_name !== $user_name) { 422 $this->user_name = $user_name; 423 424 DB::table('user') 425 ->where('user_id', '=', $this->user_id) 426 ->update([ 427 'user_name' => $user_name, 428 ]); 429 } 430 431 return $this; 432 } 433 434 /** 435 * Get the real name of this user. 436 * 437 * @return string 438 */ 439 public function getRealName(): string 440 { 441 return $this->real_name; 442 } 443 444 /** 445 * Set the real name of this user. 446 * 447 * @param string $real_name 448 * 449 * @return User 450 */ 451 public function setRealName($real_name): User 452 { 453 if ($this->real_name !== $real_name) { 454 $this->real_name = $real_name; 455 456 DB::table('user') 457 ->where('user_id', '=', $this->user_id) 458 ->update([ 459 'real_name' => $real_name, 460 ]); 461 } 462 463 return $this; 464 } 465 466 /** 467 * Get the email address of this user. 468 * 469 * @return string 470 */ 471 public function getEmail(): string 472 { 473 return $this->email; 474 } 475 476 /** 477 * Set the email address of this user. 478 * 479 * @param string $email 480 * 481 * @return User 482 */ 483 public function setEmail($email): User 484 { 485 if ($this->email !== $email) { 486 $this->email = $email; 487 488 DB::table('user') 489 ->where('user_id', '=', $this->user_id) 490 ->update([ 491 'email' => $email, 492 ]); 493 } 494 495 return $this; 496 } 497 498 /** 499 * Set the password of this user. 500 * 501 * @param string $password 502 * 503 * @return User 504 */ 505 public function setPassword($password): User 506 { 507 DB::table('user') 508 ->where('user_id', '=', $this->user_id) 509 ->update([ 510 'password' => password_hash($password, PASSWORD_DEFAULT), 511 ]); 512 513 return $this; 514 } 515 516 /** 517 * Fetch a user option/setting from the wt_user_setting table. 518 * Since we'll fetch several settings for each user, and since there aren’t 519 * that many of them, fetch them all in one database query 520 * 521 * @param string $setting_name 522 * @param string $default 523 * 524 * @return string 525 */ 526 public function getPreference($setting_name, $default = ''): string 527 { 528 if (empty($this->preferences) && $this->user_id !== 0) { 529 $this->preferences = DB::table('user_setting') 530 ->where('user_id', '=', $this->user_id) 531 ->pluck('setting_value', 'setting_name') 532 ->all(); 533 } 534 535 return $this->preferences[$setting_name] ?? $default; 536 } 537 538 /** 539 * Update a setting for the user. 540 * 541 * @param string $setting_name 542 * @param string $setting_value 543 * 544 * @return User 545 */ 546 public function setPreference($setting_name, $setting_value): User 547 { 548 if ($this->user_id !== 0 && $this->getPreference($setting_name) !== $setting_value) { 549 DB::table('user_setting')->updateOrInsert([ 550 'user_id' => $this->user_id, 551 'setting_name' => $setting_name, 552 ], [ 553 'setting_value' => $setting_value, 554 ]); 555 556 $this->preferences[$setting_name] = $setting_value; 557 } 558 559 return $this; 560 } 561} 562