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