1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Services; 21 22use Closure; 23use Fisharebest\Webtrees\Auth; 24use Fisharebest\Webtrees\Contracts\UserInterface; 25use Fisharebest\Webtrees\DB; 26use Fisharebest\Webtrees\Http\RequestHandlers\ContactPage; 27use Fisharebest\Webtrees\Http\RequestHandlers\MessagePage; 28use Fisharebest\Webtrees\Individual; 29use Fisharebest\Webtrees\Registry; 30use Fisharebest\Webtrees\User; 31use Fisharebest\Webtrees\Validator; 32use Illuminate\Database\Query\Builder; 33use Illuminate\Database\Query\JoinClause; 34use Illuminate\Support\Collection; 35use Psr\Http\Message\ServerRequestInterface; 36 37use function max; 38use function time; 39 40/** 41 * Functions for managing users. 42 */ 43class UserService 44{ 45 /** 46 * Find the user with a specified user_id. 47 * 48 * @param int|null $user_id 49 * 50 * @return User|null 51 */ 52 public function find(?int $user_id): ?User 53 { 54 return Registry::cache()->array() 55 ->remember('user-' . $user_id, static fn(): ?User => DB::table('user') 56 ->where('user_id', '=', $user_id) 57 ->get() 58 ->map(User::rowMapper()) 59 ->first()); 60 } 61 62 /** 63 * Find the user with a specified email address. 64 * 65 * @param string $email 66 * 67 * @return User|null 68 */ 69 public function findByEmail(string $email): ?User 70 { 71 return DB::table('user') 72 ->where('email', '=', $email) 73 ->get() 74 ->map(User::rowMapper()) 75 ->first(); 76 } 77 78 /** 79 * Find the user with a specified user_name or email address. 80 * 81 * @param string $identifier 82 * 83 * @return User|null 84 */ 85 public function findByIdentifier(string $identifier): ?User 86 { 87 return DB::table('user') 88 ->where('user_name', '=', $identifier) 89 ->orWhere('email', '=', $identifier) 90 ->get() 91 ->map(User::rowMapper()) 92 ->first(); 93 } 94 95 /** 96 * Find the user(s) with a specified genealogy record. 97 * 98 * @param Individual $individual 99 * 100 * @return Collection<int,User> 101 */ 102 public function findByIndividual(Individual $individual): Collection 103 { 104 return DB::table('user') 105 ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id') 106 ->where('gedcom_id', '=', $individual->tree()->id()) 107 ->where('setting_value', '=', $individual->xref()) 108 ->where('setting_name', '=', UserInterface::PREF_TREE_ACCOUNT_XREF) 109 ->select(['user.*']) 110 ->get() 111 ->map(User::rowMapper()); 112 } 113 114 /** 115 * Find the user with a specified password reset token. 116 * 117 * @param string $token 118 * 119 * @return User|null 120 */ 121 public function findByToken(string $token): ?User 122 { 123 return DB::table('user') 124 ->join('user_setting AS us1', 'us1.user_id', '=', 'user.user_id') 125 ->where('us1.setting_name', '=', 'password-token') 126 ->where('us1.setting_value', '=', $token) 127 ->join('user_setting AS us2', 'us2.user_id', '=', 'user.user_id') 128 ->where('us2.setting_name', '=', 'password-token-expire') 129 ->where('us2.setting_value', '>', time()) 130 ->select(['user.*']) 131 ->get() 132 ->map(User::rowMapper()) 133 ->first(); 134 } 135 136 /** 137 * Find the user with a specified user_name. 138 * 139 * @param string $user_name 140 * 141 * @return User|null 142 */ 143 public function findByUserName(string $user_name): ?User 144 { 145 return DB::table('user') 146 ->where('user_name', '=', $user_name) 147 ->get() 148 ->map(User::rowMapper()) 149 ->first(); 150 } 151 152 /** 153 * Callback to sort users by their last-login (or registration) time. 154 * 155 * @return Closure(UserInterface,UserInterface):int 156 */ 157 public function sortByLastLogin(): Closure 158 { 159 return static function (UserInterface $user1, UserInterface $user2) { 160 $registered_at1 = (int) $user1->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED); 161 $logged_in_at1 = (int) $user1->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE); 162 $registered_at2 = (int) $user2->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED); 163 $logged_in_at2 = (int) $user2->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE); 164 165 return max($registered_at1, $logged_in_at1) <=> max($registered_at2, $logged_in_at2); 166 }; 167 } 168 169 /** 170 * Callback to filter users who have not logged in since a given time. 171 * 172 * @param int $timestamp 173 * 174 * @return Closure(UserInterface):bool 175 */ 176 public function filterInactive(int $timestamp): Closure 177 { 178 return static function (UserInterface $user) use ($timestamp): bool { 179 $registered_at = (int) $user->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED); 180 $logged_in_at = (int) $user->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE); 181 182 return max($registered_at, $logged_in_at) < $timestamp; 183 }; 184 } 185 186 /** 187 * Get a list of all users. 188 * 189 * @return Collection<int,User> 190 */ 191 public function all(): Collection 192 { 193 return DB::table('user') 194 ->where('user_id', '>', 0) 195 ->orderBy('real_name') 196 ->get() 197 ->map(User::rowMapper()); 198 } 199 200 /** 201 * Get a list of all administrators. 202 * 203 * @return Collection<int,User> 204 */ 205 public function administrators(): Collection 206 { 207 return DB::table('user') 208 ->join('user_setting', 'user_setting.user_id', '=', 'user.user_id') 209 ->where('user_setting.setting_name', '=', UserInterface::PREF_IS_ADMINISTRATOR) 210 ->where('user_setting.setting_value', '=', '1') 211 ->where('user.user_id', '>', 0) 212 ->orderBy('real_name') 213 ->select(['user.*']) 214 ->get() 215 ->map(User::rowMapper()); 216 } 217 218 /** 219 * Get a list of all managers. 220 * 221 * @return Collection<int,User> 222 */ 223 public function managers(): Collection 224 { 225 return DB::table('user') 226 ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id') 227 ->where('user_gedcom_setting.setting_name', '=', UserInterface::PREF_TREE_ROLE) 228 ->where('user_gedcom_setting.setting_value', '=', UserInterface::ROLE_MANAGER) 229 ->where('user.user_id', '>', 0) 230 ->orderBy('real_name') 231 ->distinct() 232 ->select(['user.*']) 233 ->get() 234 ->map(User::rowMapper()); 235 } 236 237 /** 238 * Get a list of all moderators. 239 * 240 * @return Collection<int,User> 241 */ 242 public function moderators(): Collection 243 { 244 return DB::table('user') 245 ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id') 246 ->where('user_gedcom_setting.setting_name', '=', UserInterface::PREF_TREE_ROLE) 247 ->where('user_gedcom_setting.setting_value', '=', UserInterface::ROLE_MODERATOR) 248 ->where('user.user_id', '>', 0) 249 ->orderBy('real_name') 250 ->distinct() 251 ->select(['user.*']) 252 ->get() 253 ->map(User::rowMapper()); 254 } 255 256 /** 257 * Get a list of all verified users. 258 * 259 * @return Collection<int,User> 260 */ 261 public function unapproved(): Collection 262 { 263 return DB::table('user') 264 ->leftJoin('user_setting', static function (JoinClause $join): void { 265 $join 266 ->on('user_setting.user_id', '=', 'user.user_id') 267 ->where('user_setting.setting_name', '=', UserInterface::PREF_IS_ACCOUNT_APPROVED); 268 }) 269 ->where(static function (Builder $query): void { 270 $query 271 ->where('user_setting.setting_value', '<>', '1') 272 ->orWhereNull('user_setting.setting_value'); 273 }) 274 ->where('user.user_id', '>', 0) 275 ->orderBy('real_name') 276 ->select(['user.*']) 277 ->get() 278 ->map(User::rowMapper()); 279 } 280 281 /** 282 * Get a list of all verified users. 283 * 284 * @return Collection<int,User> 285 */ 286 public function unverified(): Collection 287 { 288 return DB::table('user') 289 ->leftJoin('user_setting', static function (JoinClause $join): void { 290 $join 291 ->on('user_setting.user_id', '=', 'user.user_id') 292 ->where('user_setting.setting_name', '=', UserInterface::PREF_IS_EMAIL_VERIFIED); 293 }) 294 ->where(static function (Builder $query): void { 295 $query 296 ->where('user_setting.setting_value', '<>', '1') 297 ->orWhereNull('user_setting.setting_value'); 298 }) 299 ->where('user.user_id', '>', 0) 300 ->orderBy('real_name') 301 ->select(['user.*']) 302 ->get() 303 ->map(User::rowMapper()); 304 } 305 306 /** 307 * Get a list of all users who are currently logged in. 308 * 309 * @return Collection<int,User> 310 */ 311 public function allLoggedIn(): Collection 312 { 313 return DB::table('user') 314 ->join('session', 'session.user_id', '=', 'user.user_id') 315 ->where('user.user_id', '>', 0) 316 ->orderBy('real_name') 317 ->distinct() 318 ->select(['user.*']) 319 ->get() 320 ->map(User::rowMapper()); 321 } 322 323 /** 324 * Create a new user. 325 * The calling code needs to check for duplicates identifiers before calling 326 * this function. 327 * 328 * @param string $user_name 329 * @param string $real_name 330 * @param string $email 331 * @param string $password 332 * 333 * @return User 334 */ 335 public function create(string $user_name, string $real_name, string $email, #[\SensitiveParameter] string $password): User 336 { 337 DB::table('user')->insert([ 338 'user_name' => $user_name, 339 'real_name' => $real_name, 340 'email' => $email, 341 'password' => password_hash($password, PASSWORD_DEFAULT), 342 ]); 343 344 $user_id = (int) DB::connection()->getPdo()->lastInsertId(); 345 346 return new User($user_id, $user_name, $real_name, $email); 347 } 348 349 /** 350 * Delete a user 351 * 352 * @param User $user 353 * 354 * @return void 355 */ 356 public function delete(User $user): void 357 { 358 // Don't delete the logs, just set the user to null. 359 DB::table('log') 360 ->where('user_id', '=', $user->id()) 361 ->update(['user_id' => null]); 362 363 // Take over the user’s pending changes. (What else could we do with them?) 364 DB::table('change') 365 ->where('user_id', '=', $user->id()) 366 ->where('status', '=', 'rejected') 367 ->delete(); 368 369 DB::table('change') 370 ->where('user_id', '=', $user->id()) 371 ->update(['user_id' => Auth::id()]); 372 373 // Delete settings and preferences 374 DB::table('block_setting') 375 ->join('block', 'block_setting.block_id', '=', 'block.block_id') 376 ->where('user_id', '=', $user->id()) 377 ->delete(); 378 379 DB::table('block')->where('user_id', '=', $user->id())->delete(); 380 DB::table('user_gedcom_setting')->where('user_id', '=', $user->id())->delete(); 381 DB::table('user_setting')->where('user_id', '=', $user->id())->delete(); 382 DB::table('message')->where('user_id', '=', $user->id())->delete(); 383 DB::table('user')->where('user_id', '=', $user->id())->delete(); 384 } 385 386 /** 387 * @param User $contact_user 388 * @param ServerRequestInterface $request 389 * 390 * @return string 391 */ 392 public function contactLink(User $contact_user, ServerRequestInterface $request): string 393 { 394 $tree = Validator::attributes($request)->tree(); 395 $user = Validator::attributes($request)->user(); 396 397 if ($contact_user->getPreference(UserInterface::PREF_CONTACT_METHOD) === MessageService::CONTACT_METHOD_MAILTO) { 398 $url = 'mailto:' . $contact_user->email(); 399 } elseif ($user instanceof User) { 400 // Logged-in users send direct messages 401 $url = route(MessagePage::class, [ 402 'to' => $contact_user->userName(), 403 'tree' => $tree->name(), 404 'url' => (string) $request->getUri(), 405 ]); 406 } else { 407 // Visitors use the contact form. 408 $url = route(ContactPage::class, [ 409 'to' => $contact_user->userName(), 410 'tree' => $tree->name(), 411 'url' => (string) $request->getUri(), 412 ]); 413 } 414 415 return '<a href="' . e($url) . '" dir="auto" rel="nofollow">' . e($contact_user->realName()) . '</a>'; 416 } 417} 418