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