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