1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 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\Http\RequestHandlers\ContactPage; 27use Fisharebest\Webtrees\Http\RequestHandlers\MessagePage; 28use Fisharebest\Webtrees\Individual; 29use Fisharebest\Webtrees\Tree; 30use Fisharebest\Webtrees\User; 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 app; 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 app('cache.array')->rememberForever(__CLASS__ . $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 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 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 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 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 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 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 ->get() 279 ->map(User::rowMapper()); 280 } 281 282 /** 283 * Get a list of all verified users. 284 * 285 * @return Collection 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', '=', User::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 ->get() 303 ->map(User::rowMapper()); 304 } 305 306 /** 307 * Get a list of all users who are currently logged in. 308 * 309 * @return Collection 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 ->select(['user.*']) 318 ->distinct() 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, 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 = $request->getAttribute('tree'); 395 assert($tree instanceof Tree); 396 397 $user = $request->getAttribute('user'); 398 399 if ($contact_user->getPreference(User::PREF_CONTACT_METHOD) === 'mailto') { 400 $url = 'mailto:' . $contact_user->email(); 401 } elseif ($user instanceof User) { 402 // Logged-in users send direct messages 403 $url = route(MessagePage::class, ['to' => $contact_user->userName(), 'tree' => $tree->name()]); 404 } else { 405 // Visitors use the contact form. 406 $url = route(ContactPage::class, [ 407 'to' => $contact_user->userName(), 408 'tree' => $tree->name(), 409 'url' => (string) $request->getUri(), 410 ]); 411 } 412 413 return '<a href="' . e($url) . '" dir="auto">' . e($contact_user->realName()) . '</a>'; 414 } 415} 416