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