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